一、ZK的特性:
1.写操作严格有序:所有写操作按请求顺序执行,在同一时间并发修改同一个ZNode时,只有一个请求能成功.这种特性就是俗称的master选举模式.
2.watch机制:zk支持推拉结合的发布订阅模式,可以在读取某个节点数据的同时对该节点设置监视器(原子操作),以监视从读取那一刻起该节点后续发生的数据变更.
3.临时节点:ZNode的生命周期默认是从创建那一刻起一直存在直到被删除,同时zk也支持创建临时节点,临时节点生命周期与Session会话一致,会话中断节点也随之被删除.
二、服务注册与发现
在此架构中有三类角色:服务提供者,服务注册中心,服务消费者。
服务提供者
服务提供者作为服务的提供方将自身的服务信息注册到服务注册中心中。
服务注册中心
服务注册中心主要提供所有服务注册信息的中心存储,同时负责将服务注册信息的更新通知实时的Push给服务消费者。
服务消费者
服务消费者在启动时从服务注册中心获取需要的服务注册信息,将服务注册信息缓存在本地
根据本地缓存中的服务注册信息构建服务调用请求,并根据负载均衡策略(随机负载均衡,Round-Robin负载均衡等)来转发请求
监听服务注册信息的变更,如接收到服务注册中心的服务变更通知,则在本地缓存中更新服务的注册信息
对服务提供方的存活进行检测,如果出现服务不可用的服务提供方,将从本地缓存中剔除
服务消费者只在自己初始化以及服务变更时会依赖服务注册中心,在此阶段的单点故障通过Zookeeper集群来进行保障。在整个服务调用过程中,服务消费者不依赖于任何第三方服务
代码如下:
1. 产品A服务(可以在复制产品A1,A2服务,组成集群服务);
2. 订单服务;
pom依赖
<dependencies>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.9</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<!--导入Spring坐标依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- 导入SpringMVC坐标依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<!-- 导入servlet的坐标依赖 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.2</version>
</dependency>
<!-- 单元测试的坐标依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.9</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
产品服务代码
启动类:
package com.sinosun;
import com.sinosun.listener.InitListener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
/**
* Created by VULCAN on 2018/7/28.
*/
@SpringBootApplication
public class ProductApp {
public static void main(String[] args) {
SpringApplication.run(ProductApp.class,args);
}
// 监听器
@Bean
public ServletListenerRegistrationBean servletListenerRegistrationBean() {
ServletListenerRegistrationBean servletListenerRegistrationBean = new ServletListenerRegistrationBean();
servletListenerRegistrationBean.setListener(new InitListener());
return servletListenerRegistrationBean;
}
}
监听器:
package com.sinosun.listener;
import com.sinosun.zk.ServiceRegister;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.context.support.WebApplicationContextUtils;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.net.InetAddress;
/**
* Created by VULCAN on 2018/7/28.
*/
public class InitListener implements ServletContextListener {
@Value("${server.port}")
private int port;
// 容器初始化
@Override
public void contextInitialized(ServletContextEvent sce) {
WebApplicationContextUtils.getRequiredWebApplicationContext(
sce.getServletContext()).getAutowireCapableBeanFactory().autowireBean(this);
try {
// 可能会报异常错误
//String hostAddress = InetAddress.getLocalHost().getHostAddress();
String hostAddress = "8.8.8.8";
ServiceRegister.register(hostAddress,port);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
注册服务
package com.sinosun.zk;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
/**
* Created by VULCAN on 2018/7/28.
*/
public class ServiceRegister {
private static final String BASE_SERVICES = "/services";
private static final String SERVICE_NAME = "/products";
public static void register(String address, int port) {
try {
ZooKeeper zooKeeper = new ZooKeeper("192.168.179.131:2181", 10000, (watchedEvent) -> {
});
Stat exists = zooKeeper.exists(BASE_SERVICES + SERVICE_NAME, false);
if (exists == null) {
zooKeeper.create(BASE_SERVICES + SERVICE_NAME, "testzk".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
String server_path = address + ":" + port;
// 临时顺序节点 当产品服务发生异常时(和zk断开时),可以通知到订单服务
String result = zooKeeper.create(BASE_SERVICES + SERVICE_NAME + "/child", server_path.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
控制层
package com.sinosun.controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.sinosun.pojo.Product;
import javax.servlet.http.HttpServletRequest;
/**
* Created by VULCAN on 2018/7/28.
*/
@RestController
@RequestMapping("/product")
public class ProductController {
@RequestMapping("/getProduct/{id}")
public Object getProduct(HttpServletRequest request, @PathVariable("id") String id) {
return new Product(id,"name:"+request.getLocalPort());
}
}
POJO
package com.sinosun.pojo;
/**
* Created by VULCAN on 2018/7/28.
*/
public class Product {
private String id;
private String name;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Product(String id, String name) {
this.id = id;
this.name = name;
}
public Product() {
}
}
订单服务代码
启动类在这里插入代码片
package com.sinosun;
import com.sinosun.listener.InitListener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
/**
* Created by VULCAN on 2018/7/28.
*/
@SpringBootApplication
public class OrderApp {
public static void main(String[] args) {
SpringApplication.run(OrderApp.class,args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
public ServletListenerRegistrationBean servletListenerRegistrationBean() {
ServletListenerRegistrationBean servletListenerRegistrationBean = new ServletListenerRegistrationBean();
servletListenerRegistrationBean.setListener(new InitListener());
return servletListenerRegistrationBean;
}
}
监听器
package com.sinosun.listener;
import com.sinosun.utils.LoadBalance;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.util.ArrayList;
import java.util.List;
/**
* Created by VULCAN on 2018/7/28.
*/
public class InitListener implements ServletContextListener {
private static final String BASE_SERVICES = "/services";
private static final String SERVICE_NAME="/products";
private ZooKeeper zooKeeper;
@Override
public void contextInitialized(ServletContextEvent sce) {
try {
// 如果节点有变化,则更新本地配置
zooKeeper = new ZooKeeper("192.168.179.131:2181",5000,(watchedEvent)->{
if(watchedEvent.getType() == Watcher.Event.EventType.NodeChildrenChanged && watchedEvent.getPath().equals(BASE_SERVICES+SERVICE_NAME)) {
updateServiceList();
}
});
updateServiceList();
} catch (Exception e) {
e.printStackTrace();
}
}
private void updateServiceList() {
try{
List<String> children = zooKeeper.getChildren(BASE_SERVICES + SERVICE_NAME, true);
List<String> newServerList = new ArrayList<String>();
for(String subNode:children) {
byte[] data = zooKeeper.getData(BASE_SERVICES + SERVICE_NAME + "/" + subNode, false, null);
String host = new String(data, "utf-8");
System.out.println("host:"+host);
newServerList.add(host);
}
// 将注册信息缓存到本地
LoadBalance.SERVICE_LIST = newServerList;
}catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
随机的负载均衡:
package com.sinosun.utils;
import java.util.List;
/**
* Created by VULCAN on 2018/7/28.
*/
public abstract class LoadBalance {
public volatile static List<String> SERVICE_LIST;
public abstract String choseServiceHost();
}
package com.sinosun.utils;
import org.springframework.util.CollectionUtils;
import java.util.Random;
/**
* 负载均衡(随机)
*/
public class RamdomLoadBalance extends LoadBalance {
@Override
public String choseServiceHost() {
String result = "";
if(!CollectionUtils.isEmpty(SERVICE_LIST)) {
int index = new Random().nextInt(SERVICE_LIST.size());
result = SERVICE_LIST.get(index);
}
return result ;
}
}
controller(Rest方式调用)
package com.sinosun.controller;
import com.sinosun.pojo.Order;
import com.sinosun.pojo.Product;
import com.sinosun.utils.LoadBalance;
import com.sinosun.utils.RamdomLoadBalance;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
/**
* Created by VULCAN on 2018/7/28.
*/
@RequestMapping("/order")
@RestController
public class OrderController {
@Resource
private RestTemplate restTemplate;
private LoadBalance loadBalance = new RamdomLoadBalance();
@RequestMapping("/getOrder/{id}")
public Object getOrder(@PathVariable("id") String id ) {
Product product = restTemplate.getForObject("http://"+loadBalance.choseServiceHost()+"/product/getProduct/1", Product.class);
return new Order(id,"orderName",product);
}
}
POJO
package com.sinosun.pojo;
/**
* Created by VULCAN on 2018/7/28.
*/
public class Order {
private String id;
private String name;
private Product product;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Product getProduct() {
return product;
}
public void setProduct(Product product) {
this.product = product;
}
public Order(String id, String name, Product product) {
this.id = id;
this.name = name;
this.product = product;
}
public Order() {
}
}
package com.sinosun.pojo;
/**
* Created by VULCAN on 2018/7/28.
*/
public class Product {
private String id;
private String name;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Product(String id, String name) {
this.id = id;
this.name = name;
}
public Product() {
}
}
三、集群选举(leader选举)
https://blog.csdn.net/allensandy/article/details/89928763(强烈推荐)
Controller
package com.sinosun.leader.controller;
import com.sinosun.leader.listener.ElectionMaster;
import org.apache.zookeeper.server.quorum.Election;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.lang.annotation.ElementType;
@RestController
public class indexController {
@RequestMapping("/getServiceInfo")
public String getServiceInfo() {
return ElectionMaster.issurval ? "当前服务器为主节点" : "当前服务器为从节点";
}
}
监听器
package com.sinosun.leader.listener;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import javax.servlet.ServletContextListener;
public class InitListener implements ServletContextListener, InitializingBean {
ZkClient zkClient = new ZkClient("192.168.179.131:2181");
private String path = "/eletion";
@Value("${server.port}")
private String port;
private void init() {
System.out.println("start success");
createEphemeral();
zkClient.subscribeDataChanges(path, new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
@Override
public void handleDataDeleted(String s) throws Exception {
System.out.println("主节点挂了");
Thread.sleep(5000);
createEphemeral();
}
});
}
public void createEphemeral() {
try {
zkClient.createEphemeral(path, port);
ElectionMaster.issurval = true;
} catch (Exception e) {
ElectionMaster.issurval = false;
}
}
@Override
public void afterPropertiesSet() throws Exception {
init();
}
}
package com.sinosun.leader.listener;
public class ElectionMaster {
public static boolean issurval;
}
启动类
package com.sinosun.leader;
import com.sinosun.leader.listener.InitListener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
public class LeaderApp {
public static void main(String[] args) {
SpringApplication.run(LeaderApp.class, args);
}
@Bean
public ServletListenerRegistrationBean servletListenerRegistrationBean() {
ServletListenerRegistrationBean servletListenerRegistrationBean = new ServletListenerRegistrationBean();
servletListenerRegistrationBean.setListener(new InitListener());
return servletListenerRegistrationBean;
}
}
四、分布式锁
- 锁临时节点(实现简单,但是性能较低,会发生羊群效应)
- 锁临时顺序节点(只会监听上一个临时顺序节点,效率较高)
代码实现:
公用代码:
package com.sinosun.zklock.zk;
public interface ZKLock {
void getLock();
void unLock();
}
package com.sinosun.zklock.zk;
public abstract class AbstractZKLock implements ZKLock {
public void getLock() {
if (tryLock()) {
System.out.println("获取锁成功");
} else {
// 等待锁释放
waitLock();
// 获得锁
getLock();
}
}
public abstract boolean tryLock();
public abstract void waitLock();
}
package com.sinosun.zklock.zk;
import org.I0Itec.zkclient.ZkClient;
public abstract class ZookeeperAbstractZKLock extends AbstractZKLock{
private static final String CONN= "192.168.179.131:2181";
protected static final String PATH= "/SERVER1";
protected static final String PATH2= "/SERVER2";
protected ZkClient zkClient =new ZkClient(CONN);
}
锁临时节点:
package com.sinosun.zklock.zk;
import org.I0Itec.zkclient.IZkDataListener;
import java.util.concurrent.CountDownLatch;
public class ZeekeeperLock extends ZookeeperAbstractZKLock {
private CountDownLatch countDownLatch;
@Override
// 尝试获取锁
public boolean tryLock() {
try {
// 创建临时节点
zkClient.createEphemeral(PATH);
return true;
} catch (Exception e) {
// 创建失败
return false;
}
}
@Override
public void waitLock() {
IZkDataListener listener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
@Override
public void handleDataDeleted(String s) throws Exception {
// 当临时节点被删除时,开关打开,countDownLatch减一
if (countDownLatch != null) {
countDownLatch.countDown();
}
}
};
// 注册一个事件监听
zkClient.subscribeDataChanges(PATH, listener);
if (zkClient.exists(PATH)) {
countDownLatch = new CountDownLatch(1);
try {
// 如果这个临时节点存在就一直等待
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
zkClient.unsubscribeDataChanges(PATH, listener);
}
@Override
public void unLock() {
if (zkClient != null) {
zkClient.delete(PATH);
// 关闭会立刻释放资源
zkClient.close();
System.out.println("释放锁成功");
}
}
}
锁临时顺序节点(监听前面的节点)
package com.sinosun.zklock.zk;
import org.I0Itec.zkclient.IZkDataListener;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class ZeekeeperLock2 extends ZookeeperAbstractZKLock {
private CountDownLatch countDownLatch;
private String beforePath;
private String currentPath;
public ZeekeeperLock2() {
if (!this.zkClient.exists(PATH2)) {
this.zkClient.createPersistent(PATH2);
}
}
@Override
// 尝试获取锁
public boolean tryLock() {
// 如果当前节点为空,尝试第一次加锁后赋值currentPath
if (currentPath == null || currentPath.length() <= 0) {
currentPath = this.zkClient.createEphemeralSequential(PATH2 + "/", "lock");
}
// 获取所有节点信息,并且排序
List<String> childrens = this.zkClient.getChildren(PATH2);
Collections.sort(childrens);
if (currentPath.equals(PATH2 + "/" + childrens.get(0))) {
// 如果当前节点是第一个节点,则获取锁成功
return true;
} else {
// 如果当前节点不是第一个节点,则获取前一个节点,并且赋值给beforePath
int wz = Collections.binarySearch(childrens, currentPath.substring(7));
beforePath = PATH2 + "/" + childrens.get(wz - 1);
}
return false;
}
@Override
public void waitLock() {
IZkDataListener listener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
@Override
public void handleDataDeleted(String s) throws Exception {
// 当临时节点被删除时,开关打开,countDownLatch减一
if (countDownLatch != null) {
countDownLatch.countDown();
}
}
};
// 给前面的节点增加一个监听
zkClient.subscribeDataChanges(beforePath, listener);
if (zkClient.exists(beforePath)) {
countDownLatch = new CountDownLatch(1);
try {
// 如果这个临时节点存在就一直等待
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
zkClient.unsubscribeDataChanges(PATH, listener);
}
@Override
public void unLock() {
if (zkClient != null) {
zkClient.delete(currentPath);
zkClient.close();
System.out.println("释放锁成功");
}
}
}
五、配置中心
zookeeper作为配置中心的优势在于其自身的watch机制,可以随时发现一些数据的变化,从而达到数据的及时性。也就是zookeeper可以做到,只要数据一发生变化,就会通知相应地注册了监听的客户端。
具体代码可以网上搜索。
https://www.jianshu.com/p/dbbc640900df(讲的非常详细)