从库存超卖问题分析锁和分布式锁的应用(一)
从库存超卖问题分析锁和分布式锁的应用(二)
分布式锁的最佳实践之Redisson
分布式锁的最佳实践之Zookeeper
分布式锁的最佳实践之MySQL
【1】思路分析
分布式锁的步骤:
- 获取锁:create一个节点
- 删除锁:delete一个节点
- 重试:没有获取到锁的请求重试
参照redis分布式锁的特点:
- 互斥 排他
- 防死锁:
- 可自动释放锁(临时节点) :获得锁之后客户端所在机器宕机了,客户端没有主动删除子节点;如果创建的是永久的节点,那么这个锁永远不会释放,导致死锁;由于创建的是临时节点,客户端宕机后,过了一定时间zookeeper没有收到客户端的心跳包判断会话失效,将临时节点删除从而释放锁。
- 可重入锁:借助于ThreadLocal
- 防误删:宕机自动释放临时节点,不需要设置过期时间,也就不存在误删问题。
- 加锁/解锁要具备原子性
- 单点问题:使用Zookeeper可以有效的解决单点问题,ZK一般是集群部署的。
- 集群问题:zookeeper集群是强一致性的,只要集群中有半数以上的机器存活,就可以对外提供服务。
实现思路:
- 多个请求同时添加一个相同的临时节点,只有一个可以添加成功。添加成功的获取到锁
- 执行业务逻辑
- 完成业务流程后,删除节点释放锁。
【2】简单实现
由于zookeeper获取链接是一个耗时过程,这里可以在项目启动时,初始化链接,并且只初始化一次。借助于spring特性,代码实现如下:
@Component
public class ZkClient {
private static final String connectString = "127.0.0.1:2181";
private ZooKeeper zooKeeper;
@PostConstruct
public void init(){
try {
zooKeeper = new ZooKeeper(connectString, 30000, new Watcher() {
@Override
public void process(WatchedEvent event) {
if (Event.KeeperState.SyncConnected.equals(event.getState())
&& Event.EventType.None.equals(event.getType())) {
System.out.println("获取链接成功。。。。。。" + event);
}
}
});
} catch (Exception e) {
System.out.println("获取链接失败!");
e.printStackTrace();
}
}
@PreDestroy
public void destroy(){
try {
if (zooKeeper != null){
zooKeeper.close();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 初始化zk分布式锁对象方法
* @param lockName
* @return
*/
public ZkDistributedLock getLock(String lockName){
return new ZkDistributedLock(zooKeeper, lockName);
}
}
分布式锁代码如下:
public class ZkDistributedLock {
private static final String ROOT_PATH = "/distributed";
private String path;
private ZooKeeper zooKeeper;
public ZkDistributedLock(ZooKeeper zooKeeper, String lockName){
this.zooKeeper = zooKeeper;
this.path = ROOT_PATH + "/" + lockName;
// 创建分布式锁根节点
try {
if (this.zooKeeper.exists(ROOT_PATH, false) == null){
this.zooKeeper.create(ROOT_PATH, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
// 注意,这里使用递归方法实现自旋获取锁机制。自旋竞争资源会降低吞吐量
public void lock(){
try {
// 临时节点,防止服务器宕机带来的死锁问题
zooKeeper.create(path, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
} catch (Exception e) {
// 重试
try {
Thread.sleep(200);
lock();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
public void unlock(){
try {
this.zooKeeper.delete(path, -1);
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
}
基本实现中由于无限自旋影响性能。试想:每个请求要想正常的执行完成,最终都是要创建节点,如果能够避免争抢必然可以提高性能。
【3】阻塞锁/公平锁
分析思路:这里创建临时序列化节点
- 所有请求要求获取锁时,给每一个请求创建临时序列化节点
- 获取当前节点的前置节点,如果前置节点为空则获取锁成功,否则监听前置节点
- 获取锁成功之后执行业务操作,然后释放当前节点的锁。
public class ZkDistributedLock {
private static final String ROOT_PATH = "/distributed";
private String lockName;
private String currNodePath;
private ZooKeeper zooKeeper;
public ZkDistributedLock(ZooKeeper zooKeeper, String lockName){
this.zooKeeper = zooKeeper;
this.lockName = lockName;
// 创建分布式锁根节点
try {
if (this.zooKeeper.exists(ROOT_PATH, false) == null){
this.zooKeeper.create(ROOT_PATH, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
public boolean lock(){
try {
// 临时序列化节点,防止服务器宕机带来的死锁问题。这里加 - 是为了方便截取序列号
currNodePath = zooKeeper.create(ROOT_PATH + "/" + lockName, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
//获取前置节点,如果前置节点为空则获取锁成功,否则监听前置节点。
String preNode = getPreNode();
if (StringUtils.isEmpty(preNode)){
return true;
}
//利用闭锁实现阻塞功能
CountDownLatch countDownLatch=new CountDownLatch(1);
//因为获取前置节点这个操作不具备原子性,再次判断前置节点是否存在
if(this.zooKeeper.exists(ROOT_PATH + "/" + preNode, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
countDownLatch.countDown();
}
})==null){
return true;
}
countDownLatch.await();
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
public void unlock(){
try {
this.zooKeeper.delete(currNodePath, -1);
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
/**
* 获取指定节点的前节点
* @return
*/
private String getPreNode(){
try {
// 获取根路径下的所有序列化子节点
List<String> children = this.zooKeeper.getChildren(ROOT_PATH, false);
// 判空
if (CollectionUtils.isEmpty(children)){
throw new IllegalMonitorStateException("获取前置节点失败!");
}
// 获取和当前节点统一资源的锁
List<String> nodes = children.stream().filter(node -> StringUtils.startsWith(node, lockName)).collect(Collectors.toList());
// 判空
if (CollectionUtils.isEmpty(nodes)){
throw new IllegalMonitorStateException("获取前置节点失败!");
}
//排队
Collections.sort(nodes);
//获取当前节点下标
String currNode = StringUtils.substringAfterLast(currNodePath, "/");//当前节点全路径对应的节点名称
int index = Collections.binarySearch(nodes, currNode);
if(index<0){
throw new IllegalMonitorStateException("获取前置节点失败!");
}else if (index >0){
return nodes.get(index-1);//返回前置节点
}
return null;
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
throw new IllegalMonitorStateException("获取前置节点失败!");
}
}
}
目前还未实现可重入功能,如果一个线程任务中连续两次获取同一把锁将会死锁。可以采用THREAD_LOCAL记录重入次数,实现重入机制。
【4】可重入锁
思路如下:
- 使用HREAD_LOCAL记录重入次数
- 加锁时重入次数+1
- 解锁时重入次数-1
- 如果解锁后重入次数为0,则删除节点
ublic class ZkDistributedLock {
private static final String ROOT_PATH = "/distributed";
private String lockName;
private String currNodePath;
private ZooKeeper zooKeeper;
private static final ThreadLocal<Integer> THREAD_LOCAL = new ThreadLocal<>();
public ZkDistributedLock(ZooKeeper zooKeeper, String lockName){
this.zooKeeper = zooKeeper;
this.lockName = lockName;
// 创建分布式锁根节点
try {
if (this.zooKeeper.exists(ROOT_PATH, false) == null){
this.zooKeeper.create(ROOT_PATH, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
public boolean lock(){
try {
// 判断是否拥有锁,如果是,则flag+1
Integer flag = THREAD_LOCAL.get();
if(flag!=null&&flag>0){
THREAD_LOCAL.set(++flag);//重入
return true;
}
// 临时序列化节点,防止服务器宕机带来的死锁问题
currNodePath = zooKeeper.create(ROOT_PATH + "/" + lockName, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
//获取前置节点,如果前置节点为空则获取锁成功,否则监听前置节点。
String preNode = getPreNode();
if (StringUtils.isEmpty(preNode)){
THREAD_LOCAL.set(1);
return true;
}
//利用闭锁实现阻塞功能
CountDownLatch countDownLatch=new CountDownLatch(1);
//因为获取前置节点这个操作不具备原子性,再次判断前置节点是否存在
if(this.zooKeeper.exists(ROOT_PATH + "/" + preNode, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
countDownLatch.countDown();
}
})==null){
THREAD_LOCAL.set(1);
return true;
}
countDownLatch.await();
THREAD_LOCAL.set(1);
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
public void unlock(){
try {
THREAD_LOCAL.set(THREAD_LOCAL.get()-1);
if(THREAD_LOCAL.get() ==0){
this.zooKeeper.delete(currNodePath, -1);
}
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
/**
* 获取指定节点的前节点
* @return
*/
private String getPreNode(){
try {
// 获取根路径下的所有序列化子节点
List<String> children = this.zooKeeper.getChildren(ROOT_PATH, false);
// 判空
if (CollectionUtils.isEmpty(children)){
throw new IllegalMonitorStateException("获取前置节点失败!");
}
// 获取和当前节点统一资源的锁
List<String> nodes = children.stream().filter(node -> StringUtils.startsWith(node, lockName)).collect(Collectors.toList());
// 判空
if (CollectionUtils.isEmpty(nodes)){
throw new IllegalMonitorStateException("获取前置节点失败!");
}
//排队
Collections.sort(nodes);
//获取当前节点下标
String currNode = StringUtils.substringAfterLast(currNodePath, "/");//当前节点全路径对应的节点名称
int index = Collections.binarySearch(nodes, currNode);
if(index<0){
throw new IllegalMonitorStateException("获取前置节点失败!");
}else if (index >0){
return nodes.get(index-1);//返回前置节点
}
return null;
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
throw new IllegalMonitorStateException("获取前置节点失败!");
}
}
}
【5】Zookeeper分布式锁特点
- 独占排他互斥:节点不重复
- 防死锁:客户端程序获取到锁之后服务器立马宕机。临时节点:一旦客户端服务器宕机,链接就会关闭,此时zk心跳检测不到客户端程序,删除对应的临时节点
- 防误删:给每一个请求线程创建一个唯一的序列化节点
- 原子性:创建节点 删除节点 查询及监听 具备原子性
- 可重入:ThreadLocal实现
- 自动续期:没有过期时间 也就不需要自动续期
- 单点故障:zk一般都是集群部署,zk集群偏向于一致性集群