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、源码下载