zookeeper可以作為微服務注冊中心,spring cloud也提供了zookeeper注冊中心的支持。
本文介紹如何實現一個簡單的zookeeper注冊中心,主要的實現方式:
n個服務提供者對外提供http接口獲取數據,這些服務提供者把自己的主機、端口信息注冊到zookeeper的某個節點上面;
當服務提供者宕機或者服務不可用時,zookeeper節點會刪除該提供者的信息;
消費者也連接zookeeper獲取可以使用的服務提供者(並且會持續監聽節點,節點變化時也會實時更新本地的服務提供者列表),然后發起http請求調用服務接口獲取數據
下面簡單介紹一下實現方式
1、UserService服務
后台用戶管理服務web工程,使用spring mvc注解方式開發,不提供用戶操作界面。提供http接口根據用戶名獲取用戶信息,入參用戶名字符串,響應為json類型,注冊的服務名為UserService
2、UserAdminService服務
用戶管理系統web工程,使用spring mvc注解方式開發,這是一個給用戶使用的系統。提供登錄頁面、和首頁,登錄操作調用UserService提供的http接口根據用戶輸入的用戶名獲取用戶信息,然后比對密碼是否正確,服務名為UserAdminService
3、修改UserService服務工程
1)application.properties配置
1 ## zookeeper集群地址 2 # zk.url=192.168.0.28:2181,192.168.0.29:2181,192.168.0.30:2181 3 zk.url=127.0.0.1:2181 4 5 ## 注冊服務使用的二級路徑名 6 service.name=UserService 7 ## IP根據實際部署的服務器修改 8 server.hostname=127.0.0.1 9 ## 端口根據實際監聽端口修改 10 server.port=8080 11 12 ## 注冊服務時會創建 13 ## /services/${service.name}/${server.hostname}:${server.port} 14 ## 這樣一個臨時節點,值為${service.name}
三個服務部署在一台服務器上,分別監聽7070、8080、9090,可以使用server.port參數配置
2)ZookeeperRegister類
編寫ZookeeperRegister類,根據zk集群地址、服務名、IP、端口等配置注冊服務到zookeeper集群
a) 讀取application.properties文件,加載配置
b) zkRegist方法會在依賴注入完成后由spring容器調用
c) 獲取zk集群地址、服務名、IP、端口等配置
d) 創建ZooKeeper對象,來注冊服務,也就是在zookeeper創建一個臨時節點
e) 節點規則是: /services/${service.name}/${server.hostname}:${server.port},值為: ${service.name}
f) 此類對象會被spring容器管理
3)ZookeeperRegister類代碼

1 @Component 2 @PropertySource("classpath:application.properties") 3 public class ZookeeperRegister { 4 5 private ZooKeeper zkClient; 6 7 @Autowired 8 private Environment env; 9 10 @PostConstruct 11 public void zkRegist() throws IOException, KeeperException, 12 InterruptedException { 13 14 // 獲取配置信息 15 String zkUrl = env.getProperty("zk.url"); 16 String serviceName = env.getProperty("service.name"); 17 String hostName = env.getProperty("server.hostname"); 18 int port = Integer.parseInt(env.getProperty("server.port")); 19 20 // 創建ZooKeeper對象 21 // 超時時間為2分 22 zkClient = new ZooKeeper(zkUrl, 120000, new Watcher() { 23 @Override 24 public void process(WatchedEvent event) { 25 26 } 27 }); 28 29 // path = /services/${service.name}/${server.hostname}:${server.port} 30 // value = ${service.name} 31 32 Stat exists = zkClient.exists("/services", false); 33 if (exists == null) { 34 zkClient.create("/services", "services".getBytes(), 35 Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); 36 exists = zkClient.exists("/services/UserService", false); 37 if (exists == null) { 38 zkClient.create("/services/" + serviceName, 39 serviceName.getBytes(), Ids.OPEN_ACL_UNSAFE, 40 CreateMode.PERSISTENT); 41 } 42 } 43 // 創建znode節點 44 // 臨時節點 45 zkClient.create("/services/" + serviceName + "/" + hostName + ":" 46 + port, serviceName.getBytes(), Ids.OPEN_ACL_UNSAFE, 47 CreateMode.EPHEMERAL); 48 } 49 }
4、修改UserAdminService服務工程
1)application.properties配置
1 ## zookeeper集群地址 2 # zk.url=192.168.0.28:2181,192.168.0.29:2181,192.168.0.30:2181 3 zk.url=127.0.0.1:2181
2)UserServiceImpl類
UserServiceImpl類實現Watcher和UserService接口,使用@Service標注,由spring容器管理
a) 初始化方法中創建ZooKeeper對象,連接到zk集群
b) 當ZooKeeper中服務提供者節點發生變化時調用getProviders方法重新獲取可用的服務提供者
c) getUserByUsername方法先從當前可用的提供者集合中隨機獲取一個提供者,使用httpclient發送請求獲取用戶數據返回給調用者
把UserService注入到Controller即可使用
3)UserServiceImpl類代碼

1 @Service 2 @PropertySource("classpath:application.properties") 3 public class UserServiceImpl implements UserService, Watcher { 4 5 private static Logger log = LoggerFactory.getLogger(UserServiceImpl.class); 6 7 private static final String SERVICE_URL = 8 "/UserService/user/getUserByUsername?username="; 9 private static final String SERVICE_REGIST_PATH = "/services/UserService"; 10 11 @Autowired 12 private Environment env; 13 14 private ZooKeeper zkClient; 15 private List<String> providers = new ArrayList<String>(); 16 17 @PostConstruct 18 public void init() { 19 String zkUrl = env.getProperty("zk.url"); 20 try { 21 zkClient = new ZooKeeper(zkUrl, 120000, this); 22 } catch (IOException e) { 23 throw new RuntimeException(e.getMessage(), e); 24 } 25 } 26 27 @Override 28 public User getUserByUsername(String username) { 29 30 User user = null; 31 32 // 獲取一個可以使用的服務提供者 33 String provider = getProvider(); 34 35 String url = "http://" + provider + SERVICE_URL + username; 36 37 // 發送請求獲取數據 38 CloseableHttpClient http = HttpClients.createDefault(); 39 40 HttpGet httpGet = null; 41 CloseableHttpResponse response = null; 42 InputStream in = null; 43 44 try { 45 // 構建httpget 46 httpGet = new HttpGet(new URI(url)); 47 48 // 發送請求獲取響應 49 response = http.execute(httpGet); 50 HttpEntity entity = response.getEntity(); 51 52 // 獲取輸入流和數據字符串 53 in = entity.getContent(); 54 String json = IOUtils.toString(in, "utf-8"); 55 56 log.info(String.format("請求: %s, 響應: %s", url, json)); 57 58 // 解析json字符串為user對象 59 user = json2User(json); 60 61 } catch (URISyntaxException e) { 62 e.printStackTrace(); 63 } catch (ClientProtocolException e) { 64 e.printStackTrace(); 65 } catch (IOException e) { 66 e.printStackTrace(); 67 } finally { 68 // 關閉輸入流和響應對象 69 IOUtils.closeQuietly(in); 70 IOUtils.closeQuietly(response); 71 } 72 return user; 73 } 74 75 @Override 76 public void process(WatchedEvent event) { 77 getProviders(); 78 } 79 80 private void getProviders() { 81 try { 82 this.providers = zkClient.getChildren(SERVICE_REGIST_PATH, true); 83 log.info(String.format("可用服務提供者: %s", this.providers)); 84 } catch (KeeperException e) { 85 e.printStackTrace(); 86 } catch (InterruptedException e) { 87 e.printStackTrace(); 88 } 89 } 90 91 private String getProvider() { 92 int size = this.providers.size(); 93 Random r = new Random(); 94 int i = r.nextInt(size); 95 String provider = this.providers.get(i); 96 log.info(String.format("可用服務提供者: %s, 隨機獲取提供者 [%s], 序號 [%s]", 97 this.providers, provider, i)); 98 return provider; 99 } 100 101 private User json2User(String json) { 102 ObjectMapper mapper = new ObjectMapper(); 103 User user = null; 104 try { 105 user = mapper.readValue(json, User.class); 106 } catch (JsonParseException e) { 107 e.printStackTrace(); 108 } catch (JsonMappingException e) { 109 e.printStackTrace(); 110 } catch (IOException e) { 111 e.printStackTrace(); 112 } 113 return user; 114 } 115 }
5、部署測試
1)部署啟動zookeeper
2)服務部署
服務提供者和消費者是部署在同一台機器上面的,監聽端口不同
部署三個UserService服務,修改server.port參數,分別監聽7070、8080、9090
部署UserAdminService服務,我測試的時候和其中一個服務提供者部署在了同一個tomcat里面,監聽9090
其中7070和8080的tomcat使用cmd命令行啟動,9090的tomcat在eclipse里面啟動便於觀察服務消費者的日志
啟動了三個tomcat之后可以看看zookeeper的變化
另外,在UserAdminService服務的日志中可以看到當前可以使用的服務提供者列表
訪問一下http://localhost:9090/UserAdminWeb/login登錄,可以看到后台程序獲取到了一個可以使用的服務提供者,然后發起了http請求查詢用戶信息
當某個服務提供者掛掉之后,可以看到消費者的日志,已經重新獲取了提供者列表
6、存在的一些問題
a) 服務提供者突然掛掉之后,zookeeper並不能馬上刪除該提供者的znode信息,所以在服務消費者這邊還需要做一些優化,即發起請求獲取用戶信息時如果出現問題,應該從可用服務提供者列表刪除該提供者信息
b) 服務消費者使用的httpclient發起請求,如果每次獲取用戶信息都去重新建立tcp連接,效率很低,所以需要使用連接池技術管理用於發起http請求的tcp連接
7、源碼下載