众所周知,JVM的并发包中提供了大量的锁工具类,比如说像:synchronized,volatile,lock等等来帮助我们实现线程的安全,但是随着技术和架构的日益发展,现在面临的问题并不只是JVM的线程安全问题,现在的系统架构基本上都是分布式部署的,那就相当于会有多个JVM进程,因此如何保证分布式系统中线程安全性又是一个问题,不过很幸运的是,当今很多开源的框架都可以帮我们实现分布式锁,比如说像redis,zk等等,但是有些时候不能只是会用才行,还要知其所以然才不会踩坑,不会出现大的线上故障,今天我们就主要来聊聊redis和zk分布式锁的实现方式,以及优缺点和使用场景。
redis自带的一个setnx命令就可以实现简单的加锁功能,实现的原理:Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,并且多个客户端对redis的来接并不会带来竞争关系,因此setnx(set if not exists)可以实现分布式锁。
1.setnx key value,返回1说明不存在,返回0说明存在,获取锁失败(因此也可以通过setnx来做高并发下的判重操作)。
2.当获取锁成功,处理完成之后,还需要释放锁:del key
3.由于加锁和解锁不是原子操作,因此可能出现中间状态,就是加锁成功一直未解锁,导致死锁出现;基于这个问题,redis提供了另外一个命令,可以增加过期时间;另外自己也可以考虑写逻辑代码通过超时时间去处理。
在用一种框架或者工具实现自身需求之前也要了解这个框架或者工具可能会带来的一系列问题才会让自己的系统更加稳定和健壮。
1.首先要注意的一个问题是获取锁是非堵塞的,无论成功还是失败都是直接返回。
这个问题可以通过while true类似的机制去实现。
2.在上述问题的解决方案中,通过while true机制实现的时候要考虑增加超时时间,避免一直空轮训。
3.在高并发场景下,设置的过期时间不能过长,否则会影响后续线程执行。
4.无法实现公平锁,可以通过写业务逻辑来实现,可以将所有等待线程放到一个队列中去实现,参照AQS公平所实现方式。
5.也无法实现可重入锁,也需要写业务逻辑代码去实现,同样可以参照AQS的实现。
下面基于Jedis客户端来实现简单的分布式锁加锁和释放锁的过程,此处只是提供一种思路,具体场景还要具体分析。
/**
* 加锁
* @param locaName
* @param acquireTimeout
* @param timeout
* @return
*/
public boolean lock(String locaName, long acquireTimeout, long timeout) {
Jedis jedis = null;
try {
// 获取连接
jedis = jedisPool.getResource();
// 锁名key值
String lockKey = "lock-" + locaName;
// 设置value
String value = "lock";
// 锁key值超时时间
int lockKeyExTime = (int) (timeout / 1000);
// 设置获取锁的超时时间,避免while长时间的空轮训
long end = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < end) {
if (jedis.setnx(lockKey, value, lockKeyExTime, NX) == 1) {
return true;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return true;
}
/**
* 释放锁
* @param lockName
* @return
*/
public boolean releaseLock(String lockName) {
Jedis conn = null;
String lockKey = "lock:" + lockName;
boolean retFlag = false;
try {
conn = jedisPool.getResource();
while (true) {
// 监视lock,准备开始事务
conn.watch(lockKey);
// 释放锁
if (StringUtils.isNotBlank(conn.get(lockKey))) {
Transaction transaction = conn.multi();
transaction.del(lockKey);
List
zk是通过临时有序(顺序)节点实现的分布式锁,所谓的临时顺序节点就是zk根据创建的时间给该节点名称进行编号。
在描述分布式锁原理之前先介绍一下几个关于节点的概念:
1.有序节点:假设当前有一个父节点为/lock,我们可以在这个父节点下面创建子节点;zk提供了一个可选的有序特性,例如我们可以创建子节点“/lock/node-”并且指明有序,那么zk在生成子节点的时候会根据当前子节点数量自动添加整数序号,也就是说第一个创建的子节点,那么生成的子节点为/lock/node-0000000000,下一个节点则为:/lock/node-0000000001,一次类推。
2.临时节点:客户端可以简历一个临时节点,在会话结束或者会话超时,zk会自动删除该节点。
3.事件监听:在读取数据时,我们可以同时对节点设置事件监听,当节点数据或者结构变化时,zk会通知客户端。当前zk有如下四种事件:(1)节点创建;(2)节点删除;(3)节点数据修改;(4)子节点变更。
实现原理:zk通过创建临时有序节点实现上锁,只有序号最小(或者顺序最靠前)的可以成功获取锁;如果该序号不是最小(顺序不是最靠前)则向它前一个节点注册Watcher,通过watch来监听前一个节点是否存在,等待watch事件(即监听节点的状态变化),如果监听到watch事件发生,则再次判断该节点是否为序号最小的节点,如果是成功获取锁,否则继续监听。
1.临时有序节点,临时节点可以避免锁的中间状态(即永久持有该锁);有序可以保证锁的公平问题;
2.通过watch实现堵塞。
性能方面不如Redis,性能开销相对较大,因为每次在创建所和释放锁的过程中,都要动态创建、销毁临时节点来实现锁功能,另外zk中创建和删除节点只能通过leader服务器来执行,然后将数据同步到所有的follower机器上,所以性能开销大。
虽然通过zk提供的API非常简洁,但是实现一个分布式锁还是比较麻烦,因此可以通过Curator这个开源项目来实现分布式锁;具体内容大家可以谷歌,此处就不详细介绍了。
实现方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Redis cluster | 性能高 | 1.实现复杂 2.安全性低 |
高并发的分布式锁实现 |
Zookeeper | 1.实现简单 2.等待锁队列,提升抢占锁效率 |
1.添加删除节点性能较低 | 并发量小,安全性比较高的业务场景 |