赞
踩
一、引言:
上一篇文章,介绍了:《学会Zookeeper分布式锁--让面试官对你刮目相看》
网上的分布式锁文章千篇一律,而此文从实际高并发场景深入浅出,缘由剖析,不管是应对面试官的层层"逼问",还是实际项目,相信都能游刃有余,你学会了吗?还不会建议请先去看下哦。
1、分布式锁,场景描述:
分布式锁用途:在分布式环境下协同共享资源的使用。
2、分布式锁思路分析
锁特点:
排他性:同一时间,只有一个线程能获得;
阻塞性:其它未抢到的线程阻塞等待,直到锁被释放,再继续抢;
可重入性:线程获得锁后,后续是否可重复获取该锁(避免死锁)。
当然,还要考虑性能开销等问题。
3、常规的分布式锁解决方案有哪几种:
文件系统:同一个目录下,不能存在同名文件
数据库锁:主键 、 唯一约束 、for update
基于Redis的分布式锁:setnx、set、Redisson
基于ZooKeeper的分布式锁:类似文件系统
对比分析:
使用数据库锁会有单机性能、单机故障等问题,锁没有失效时间,容易出现死锁,当然可以部署群集,也会出现各种各样的问题,性能开销高,这里不详细介绍。
Redis缓存实现分布式锁,相对复杂,因为没有类似zk的watch监听通知机制,需要自己另外实现;而且Redis可能会出现死锁(或短时间内死锁),比如,获取到锁的线程挂了,必须等到该节点过期时间到了,才能删除。
而Zookeeper分布式锁可靠性比Redis好,实现相对简单,但由于需要创建节点、删除节点等,所以效率相比Redis要低。
那我们在实际项目中如何选择呢?
原则上如果并发量不是特别大,追求可靠性,那么首选zookeeper。而Redis实现的分布式锁响应更快,对并发的支持性能更好,如果为了效率,首选redis实现。
上篇文章已经介绍了使用原生的Zookeeper实现的分布式锁方案,本文将讲解使用现成的框架Curator实现的分布式锁方案。
二、Curator简介
Zookeeper已经流行了这么多年,实际上基于zk的分布式锁目前已经有现成的实现框架,Curator就是Netflix开源的一套ZooKeeper客户端框架,它提供了zk场景的绝大部分实现,使用Curator就不必关心其内部算法,Curator提供了来实现分布式锁,用方法获取锁,以及用方法释放锁,同其他锁一样,方法需要放在finally代码块中,确保锁能正确释放。
ZooKeeper可以被用来实现分布式锁,具体是使用“临时顺序节点”实现(假如使用“临时节点”将会出现惊群效应,上篇有介绍)。
Curator提供了四种分布式锁,分别是:
InterProcessMutex:分布式可重入排它锁
InterProcessSemaphoreMutex:分布式排它锁
InterProcessReadWriteLock:分布式读写锁
InterProcessMultiLock:将多个锁作为单个实体管理的容
获取锁
我们可以在Zookeeper下创建一个指定的父节点作为分布式锁,每个zk客户端尝试连接zk服务获取分布式锁时,都将在此父节点下创建一个临时顺序节点,分两种情况:
如果创建的临时顺序节点是父节点下的首个子节点(最小),则获取锁成功,执行相应业务逻辑,然后释放锁。
如果创建的临时顺序节点并不是该父节点下最小的子节点,则去对比比自己小的节点注册watcher监听,只监听比自己小的上一个节点,进入阻塞等待。当前一个节点被删除时会触发Watch事件,进而唤醒当前阻塞线程。
如果前一个节点对应的客户端崩溃了,则节点对应的Watch事件也会触发,也会唤醒后一个节点对应的客户端线程,此时仍需要判断当前节点是第一个节点之后才能获取锁,否则继续进入阻塞并Watch前一个节点。
重入性
只考虑同一个客户端、同一个线程获取同一个分布式锁的可重入性,第一次获取锁成功之后,在JVM内存中的一个ConcurrentMap中存储当前线程对应的锁路径及重入次数,后面同一个线程再次获取锁时,先检查该Map中当前锁是否已被当前线程占用即可,如果已占用,则只需要递增重入次数即可。
因为重入性只考虑同一个客户端、同一个JVM、同一个线程,所以可以不用考虑判断ConcurrentMap中的Owner线程的并发问题。
释放锁
释放锁时,对应可重入分布式锁,首先重入次数减一,然后判断重入次数是否已经为0:
如果重入次数为0,则删除当前客户端线程对应的临时顺序节点,删除操作会触发次节点的Watch事件,如果有别的客户端线程正在阻塞等待,则会通过Watch机制唤醒。
如果重入次数非0,则说明还未完全释放锁,直接返回即可。
1、pom引入如下curator依赖
<!-- curator:zk客户端 -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
2、yml配置文件
curator:
connectionTimeoutMs: 5000 # 连接超时时间
elapsedTimeMs: 5000 #重试间隔时间
retryCount: 3 #重试次数
sessionTimeoutMs: 60000 # session超时时间
connectString: 127.0.0.1:2181 # zookeeper 地址
3、curator配置类,读取配置属性,并注册bean到spring ioc容器
CuratorFrameworkFactory类提供了两个方法,一个工厂方法newClient,一个构建方法build。使用工厂方法newClient可以创建一个默认的实例, 而build构建方法可以对实例进行定制。当CuratorFramework实例构建完成, 紧接着调用start()方法。
@Configuration
@ConfigurationProperties(prefix = "curator")
@Data
public class CuratorConfig {
private int retryCount;
private int elapsedTimeMs;
private String connectString;
private int sessionTimeoutMs;
private int connectionTimeoutMs;
@Bean(initMethod = "start")
public CuratorFramework curatorFramework() {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(elapsedTimeMs, retryCount);
CuratorFramework curatorFramework = CuratorFrameworkFactory.builder()
.connectString(connectString)
.sessionTimeoutMs(sessionTimeoutMs)
.retryPolicy(retryPolicy)
.build();
return curatorFramework;
}
}
4、新建一个订单服务实现类
@Slf4j
@Service
public class CuratorDisLockOrderServiceImpl implements OrderService {
private static OrderCodeGenerator codeGenerator = new OrderCodeGenerator();
private static String LOCK_PATH = "/distribute-lock";
@Autowired
private CuratorFramework curatorFramework;
@Override
public String createOrder() {
String orderCode = "";
InterProcessMutex lock = new InterProcessMutex(curatorFramework, LOCK_PATH);
try {
lock.acquire();
//生成订单编号
orderCode = codeGenerator.getOrderCode();
log.info(Thread.currentThread().getName()+"-->获取锁成功-->生成订单编号:{}",orderCode);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
lock.release();
log.info(Thread.currentThread().getName() + "-->释放锁成功。");
} catch (Exception e) {
e.printStackTrace();
}
}
// TODO 具体写自己的生成订单业务
return orderCode;
}
}
注:InterProcessMutex通过在zookeeper的某路径节点下创建临时顺序节点来实现分布式锁,即每个线程(跨进程的线程)获取同一把锁前,都需要在同样的路径下创建一个节点,节点名字由uuid + 递增序列组成。而通过对比自身的序列数是否在所有子节点的第一位,来判断是否成功获取到了锁。当获取锁失败时,它会添加watcher来监听前一个节点的变动情况,然后进行等待状态。直到watcher的事件生效将自己唤醒,或者超时时间异常返回。
5、新建一个controller,提供一个下单http接口方法
@RestController
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 模拟高并发场景,多线程,并发下单
* @return
*/
@RequestMapping("/order")
public String createOrdertTest(){
//并发线程数
int count = 20;
//循环屏障
CyclicBarrier cb = new CyclicBarrier(count);
//模拟高并发场景,多线程,创建订单
for(int i=0; i<count; i++){
new Thread(new Runnable() {
@Override
public void run() {
log.info(Thread.currentThread().getName()+"--我已经准备好了");
try {
//等待所有线程启动准备好,才一起往下执行
cb.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
//创建订单
orderService.createOrder();
}
}).start();
}
return "ok";
}
}
6、运行应用,访问 http://localhost:8080/order ,观察控制台,订单编号没有重复。
-->是不是感觉比Zookeeper原生实现的分布式锁简单0.0
有兴趣了解Curator源码是如何实现分布式锁,可以参考如下图示:
参考资料:https://blog.csdn.net/xuefeng0707/article/details/80588855
看完了,是不是又学会一个技能,点个赞,关注如下公众号 “阿甘正专” 下再走吧0.0
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。