▌前提摘要
本文基于zokkeper集群&redis数据库
-- zk1 192.168.0.211:2181
-- zk1 192.168.0.211:2181
-- zk1 192.168.0.211:2181
-- redis 192.168.1.211:6379
zookeeper集群搭建:https://blog.csdn.net/qq_37936542/article/details/107096985
redis安装:https://blog.csdn.net/qq_37936542/article/details/78522728
springboot集成redis:https://blog.csdn.net/qq_37936542/article/details/80104308
项目主要依赖
org.springframework.boot
spring-boot-starter-data-redis
redis.clients
jedis
com.101tec
zkclient
0.10
▌ zookeeper实现分布式锁流程图
1:在/locks节点下创建临时序列节点
2:调用getChildren(“l/ocks”)来获取locks下面的所有子节点
3:如果该节点的序列号是所有子节点中最小,,那么就认为客户端获取到锁,之后执行相应的业务逻辑,业务执行完毕后,删除临时节点释放锁
4:反之需该节点需要去持续监听上一节点的删除事件,直到事件被触发,程序重新回到第二步逻辑
▌ 代码实现
InterfaceLock:定义获取锁&释放锁的方法接口
public interface InterfaceLock {
// 获取锁
public void attemptLock();
// 释放锁
public void unLock();
}
AbstractLock:抽象一个父类,规定好抽象方法以及定义需要使用的变量
import java.util.concurrent.CountDownLatch;
import org.I0Itec.zkclient.ZkClient;
public abstract class AbstractLock implements InterfaceLock {
// zookeeper集群地址
protected String zkServers = "192.168.0.211:2181,192.168.0.212:2181,192.168.0.213:2181";
// 连接zookeeper
protected ZkClient zkClient = new ZkClient(zkServers);
// 定义父节点
protected String rootPath = "/locks";
// 定义锁节点(临时序列节点)的前缀
protected String lockPrefix = "lock_";
// 计数器,目的让线程等待,实现持续监听
protected CountDownLatch countDownLatch;
// 定义所节点变量
protected String lockPath;
/**
* 获取锁包括两个阶段方法 1:创建临时序列化节点 2:尝试获取锁
*/
@Override
public void attemptLock() {
// 创建锁节点
createLock();
// 尝试获取锁
tryLock();
}
// 创建锁节点的抽象方法
abstract void createLock();
// 尝试获取锁的抽象方法
abstract void tryLock();
/**
* 解锁:关闭客户端后临时节点自动删除
*/
@Override
public void unLock() {
System.out.println(Thread.currentThread().getName() + "释放锁");
if (zkClient != null)
zkClient.close();
}
}
DistributeLock:分布式锁实现类
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import org.I0Itec.zkclient.IZkDataListener;
public class DistributeLock extends AbstractLock {
@Override
void createLock() {
// 如果父节点不存在则创建
if (!zkClient.exists(rootPath))
zkClient.createPersistent(rootPath);
// 创建临时序列化的锁节点
lockPath = zkClient.createEphemeralSequential(rootPath + "/" + lockPrefix,
Thread.currentThread().getName().getBytes());
}
@Override
void tryLock() {
// 获取所有锁节点集合
List childs = zkClient.getChildren(rootPath);
// 排序一波
Collections.sort(childs);
// 获取当前锁节点在集合里面的索引
int index = childs.indexOf(lockPath.substring(rootPath.length() + 1));
// 若索引最小,获取锁
if (index == 0) {
System.out.println(Thread.currentThread().getName() + "获得锁");
return;
}
// 获取排在它前一位的锁节点
String preLockPath = childs.get(index - 1);
System.out.println(Thread.currentThread().getName() + "等待前锁释放");
// 如果不存在,再去尝试获取锁
if (!zkClient.exists(rootPath + "/" + preLockPath))
tryLock();
// 存在,监听前节点删除事件
else {
// 创建事件监听对象
IZkDataListener iZkDataListener = new IZkDataListener() {
public void handleDataDeleted(String path) throws Exception {
// 监听到节点被删除,线程继续走
if (countDownLatch != null) {
countDownLatch.countDown();
}
}
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
// TODO Auto-generated method stub
}
};
// 注册事件监听
zkClient.subscribeDataChanges(rootPath + "/" + preLockPath, iZkDataListener);
// 创建CountDownLatch对象
countDownLatch = new CountDownLatch(1);
try {
// 在这里让线程暂停等待,监听到前节点删除后,再往下走
countDownLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
// 获取锁
tryLock();
}
}
}
至此,zookeeper实现分布式代码就已经实现了,下面模拟案例
▌ 案例图解
不加锁的抢购代码:SedKillController(两个服务器的代码一致)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.mote.service.RedisService;
@RestController
public class SedKillController {
@Autowired
private RedisService redisService;
private String key = "goods";
@GetMapping("sedkill")
public String sedKill() {
// 获取商品数量
Object obj = redisService.get(key);
int mount = (int) obj;
// 如果商品被抢完,直接返回
if (mount < 0 || mount == 0) {
return "很遗憾,商品已被抢完";
}
// 线程睡眠,目的在于放大错误
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 抢到商品后,将redis的商品数量减一
redisService.set(key, --mount);
// 打印,以便观察
System.out.println(Thread.currentThread().getName() + ":抢到第" + (mount + 1) + "件商品");
return "恭喜,商品抢购成功";
}
}
启动8080、8081服务,打开浏览器请求两个服务器的接口进行测试,观察打印结果
8080打印:
8081打印:
完犊子,这都是写啥,下面我们使用常用的synchronized锁试试效果,修改sedkill方法
在重新测试,查看打印结果,不要忘记将redis的商品数据重置为10奥
8080打印:
8081打印:
咦,有惊喜,比之前打印结果好看多了,至少单服务器不会出现抢购同一商品的情况了,但是还是有问题,两台服务器竟然可以抢到同一件商品,这是绝对不允许的,下面我们尝试加上刚写好的分布式锁尝试一下
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.mote.service.RedisService;
import com.mote.zk.lock.DistributeLock;
@RestController
public class SedKillController {
@Autowired
private RedisService redisService;
private String key = "goods";
@GetMapping("sedkill")
public String sedKill() {
// 获取分布式锁对象
DistributeLock lock = new DistributeLock();
// 上锁
lock.attemptLock();
// 获取商品数量
Object obj = redisService.get(key);
int mount = (int) obj;
// 如果商品被抢完,直接返回
if (mount < 0 || mount == 0) {
// 解锁(不要忘了这里解锁)
lock.unLock();
return "很遗憾,商品已被抢完";
}
// 线程睡眠,目的在于放大错误
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 抢到商品后,将redis的商品数量减一
redisService.set(key, --mount);
// 打印,以便观察
System.out.println(Thread.currentThread().getName() + ":抢到第" + (mount + 1) + "件商品");
// 解锁
lock.unLock();
return "恭喜,商品抢购成功";
}
}
8080打印:
8081打印:
tip:挺有意思的,可以去玩玩