SpringBoot整合Zookeeper集群下实现分布式锁:
首先简单了解一下Zookeeper的分布式锁的执行流程:Zookeeper原理请看后续文章…
第一步:搭建环境
在linux系统上通过docker的docker-compose.yml文件快速部署zookeeper集群:
docker-compose.yml:通过docker-compose up -d 命令启动
version: '3.1'
services:
zoo1:
image: daocloud.io/atsctoo/zookeeper:3.4.8-all-broker
container_name: zoo1
ports:
- "2181:2181"
environment:
ZOO_MY_ID: 1
ZOO_SERVERS: server.1=zoo1:2888:3888 server.2=zoo2:2888:3888 server.3=zoo3:2888:3888
zoo2:
image: daocloud.io/atsctoo/zookeeper:3.4.8-all-broker
container_name: zoo2
ports:
- "2182:2181"
environment:
ZOO_MY_ID: 2
ZOO_SERVERS: server.1=zoo1:2888:3888 server.2=zoo2:2888:3888 server.3=zoo3:2888:3888
zoo3:
image: daocloud.io/atsctoo/zookeeper:3.4.8-all-broker
container_name: zoo3
ports:
- "2183:2181"
environment:
ZOO_MY_ID: 3
ZOO_SERVERS: server.1=zoo1:2888:3888 server.2=zoo2:2888:3888 server.3=zoo3:2888:3888
启动成功效果:
利用zookeeper的可视化工具: 可以查看zookeeper中节点,但是权限只时查看,不能进行增删改操作。
工具下载: https://blog.csdn.net/u014636209/article/details/109194920
链接: https://pan.baidu.com/s/1av1cXc8lc6I347NaD6cNfg
密码: n5p5
点击右上角的添加操作,可视化工具连接效果:
第二部:SpringBoot整合Zookeeper
gitee源码地址:https://gitee.com/xzq25_com/zookeeper-exercise
利用zookeeper实现分布式锁代理示例:
public class DistributedLock {
// 设置zookeeper连接(docker部署zookeeper集群)
private final String connectString = "120.76.159.196:2181,120.76.159.196:2181,120.76.159.196:2181";
// 设置超时时间
private final int sessionTimeout = 5000;
// 声明zookeeper
private final ZooKeeper zk;
// CountDownLatch使用场景
// 线程计数器 用于线程执行任务,计数 等待线程结束
private CountDownLatch countDownLatch = new CountDownLatch(1);
private CountDownLatch waitLatch = new CountDownLatch(1);
// 定义该临时节点上一个节点的路径
private String waitPath;
// 定义临时节点
private String node;
public DistributedLock() throws IOException, InterruptedException, KeeperException {
// 获取连接
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
// 如果连接上zk的话便可以对countDownLatch进行释放
if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
countDownLatch.countDown();
}
// 如果上一个节点进行了删除节点的操作后则可以对监听进行释放
if(watchedEvent.getType()==Event.EventType.NodeDeleted && watchedEvent.getPath().equals(waitPath)){
waitLatch.countDown();
System.out.println("当前线程已释放锁,监听当前节点的后一个节点的线程准备获取锁");
}
}
});
// 等待zk连接后才会继续往下执行
countDownLatch.await();
// 判断根节点/locks是否存在
Stat stat = zk.exists("/locks", false);
// 如果节点不存在
if (stat == null) {
zk.create("/locks", "locks".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}else{
int version = stat.getVersion();
long id = Thread.currentThread().getId();
System.out.println(version+": 线程【"+id+"】绑定zookeeper初始化成功");
}
}
/**
* 一:CreateMode类型分为4种
*1.PERSISTENT--持久型
*2.PERSISTENT_SEQUENTIAL--持久顺序型
*3.EPHEMERAL--临时型
* 4.EPHEMERAL_SEQUENTIAL--临时顺序
*
* 二:节点访问权限:
* 1.OPEN_ACL_UNSAFE:表示当前节点可以被所有客户端访问。
*/
// 对zk加锁
public void zkLock() {
// 创建节点(临时带序号的)
try {
// 当前线程id
long curThreadId = Thread.currentThread().getId();
node = zk.create("/locks/" + "seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("id为:【"+curThreadId+"】创建的节点路径:"+node);
// 判断节点是否是最小的序号节点,如果是的话就获取到锁;如果不是,监听到他序号前一个节点
List<String> children = zk.getChildren("/locks", false);
// 用于判断children中的值
// 如果只有一个值,你就直接获取锁;如果有多个节点,则需要判断谁最小
if (children.size() == 1) {
return;
} else {
// 对获取到的节点进行排序方便持续获取节点
Collections.sort(children);
// 获取节点的名称 截取掉节点的前缀
String thisNode = node.substring("/locks/".length());
// 通过该节点的名称获取该节点在集合中的位置
int index = children.indexOf(thisNode);
System.out.println(index+":"+thisNode);
// 对节点所在的索引进行判断
if (index == -1) {
System.out.println("数据出现错误");
} else if (index == 0) {
// 只有一个节点可以直接获取锁
return;
} else {
// 需要监听他前一个节点的变化
waitPath = "/locks/" + children.get(index - 1);
// 通过获取前一个节点的路径对这个节点进行监听
zk.getData(waitPath, true, null);
// 等待监听
waitLatch.await();
System.out.println( "监听到前一个线程已结束,节点已删除,当前线程获取锁");
return;
}
}
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 对zk解锁
public void unZkLock() {
try {
// 删除节点
zk.delete(node,0);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
}
注意,测试多线程的情况不能再@Test方法中测试,会报错,具体原因自定查找,这里放在mian方法中
public class ZkperLockRun {
public static void main(String[] args){
new Thread(new Runnable() {
@Override
public void run() {
try {
final DistributedLock lock1 = new DistributedLock();
long id = Thread.currentThread().getId();
lock1.zkLock();
System.out.println("线程【"+id+"】启动,获取到锁");
System.out.println("执行商品秒杀业务代码.......");
Thread.sleep(5 * 1000);
System.out.println("线程【"+id+"】结束,释放锁");
lock1.unZkLock();
}catch (Exception e){
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
final DistributedLock lock2 = new DistributedLock();
long id = Thread.currentThread().getId();
lock2.zkLock();
System.out.println("线程【"+id+"】启动,获取到锁");
System.out.println("执行商品秒杀业务代码.......");
Thread.sleep(5 * 1000);
System.out.println("线程【"+id+"】结束,释放锁");
lock2.unZkLock();
}catch (Exception e){
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
final DistributedLock lock3 = new DistributedLock();
long id = Thread.currentThread().getId();
lock3.zkLock();
System.out.println("线程【"+id+"】启动,获取到锁");
System.out.println("执行商品秒杀业务代码.......");
Thread.sleep(5 * 1000);
System.out.println("线程【"+id+"】结束,释放锁");
lock3.unZkLock();
}catch (Exception e){
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
final DistributedLock lock4 = new DistributedLock();
long id = Thread.currentThread().getId();
lock4.zkLock();
System.out.println("线程【"+id+"】启动,获取到锁");
System.out.println("执行商品秒杀业务代码.......");
Thread.sleep(5 * 1000);
System.out.println("线程【"+id+"】结束,释放锁");
lock4.unZkLock();
}catch (Exception e){
e.printStackTrace();
}
}
}).start();
}
}
控制台顺序打印结果如下:
节点当前数据版本Version:0: 线程【23】绑定zookeeper初始化成功
节点当前数据版本Version:0: 线程【21】绑定zookeeper初始化成功
节点当前数据版本Version:0: 线程【20】绑定zookeeper初始化成功
节点当前数据版本Version:0: 线程【22】绑定zookeeper初始化成功
线程id为:【21】创建的节点路径:/locks/seq-0000000120
线程id为:【22】创建的节点路径:/locks/seq-0000000121
线程id为:【20】创建的节点路径:/locks/seq-0000000123
线程id为:【23】创建的节点路径:/locks/seq-0000000122
线程【21】节点存储下标:【0】: seq-0000000120
线程【21】启动,获取到锁
执行商品秒杀业务代码.......
线程【22】节点存储下标:【1】: seq-0000000121
线程【20】节点存储下标:【3】: seq-0000000123
线程【23】节点存储下标:【2】: seq-0000000122
线程【21】结束,释放锁
当前线程已释放锁,监听当前节点的后一个节点的线程准备获取锁
监听到前一个线程已结束,节点已删除,当前线程获取锁
线程【22】启动,获取到锁
执行商品秒杀业务代码.......
线程【22】结束,释放锁
当前线程已释放锁,监听当前节点的后一个节点的线程准备获取锁
监听到前一个线程已结束,节点已删除,当前线程获取锁
线程【23】启动,获取到锁
执行商品秒杀业务代码.......
线程【23】结束,释放锁
当前线程已释放锁,监听当前节点的后一个节点的线程准备获取锁
监听到前一个线程已结束,节点已删除,当前线程获取锁
线程【20】启动,获取到锁
执行商品秒杀业务代码.......
线程【20】结束,释放锁
简单解释一下打印的顺序思路:
这里测试用例给的是4个线程并发执行,4个线程首先都连接上了zk,初始化成功,接下来4个线程都再zk上创建了zk节点,注意,再zk上创建过程是原子性的,一个一个创建的,这里是不涉及多线程安全问题,创建的节点的顺序已经监听如下图所示:
之后 21线程获取到锁,执行完业务代码之后再释放锁(在运行结果中可以看出21线程获取锁之后,紧接着其他3个线程在zookeeper上创建了节点,是因为在线程获取锁之后就让当前线程休眠了5秒,5秒足以让后面其他线程在zookeeper上创建节点了,注意这里只是创建了节点,并没有拿到锁,所以没有并发安全问题),此时21释放锁,21的监听撤销,22监听到21释放锁,22就开始获取锁,以此类推,每个线程的节点总是监听前一个节点的变化,如果前一个节点删除了,撤销监听后,当前节点的线程就可以获取锁执行业务代码了。注意:总是最小节点获取锁,Zookeeper做分布式锁执行速度相对于Redis实现分布式锁的速度底,性能相对差,一般不用Zookeeper做分布式锁,Zookeeper可以用在分布式微服务下服务统一治理以及一致性调度问题上等