基于Redis分布式锁
思路大概是这样的:在redis中设置一个值表示加了锁,然后释放锁的时候就把这个key删除。具体代码如下:
// 获取锁
// NX是指如果key不存在就成功,key存在返回false,PX可以指定过期时间
SET anyLock unique_value NX PX 30000
// 释放锁:通过执行一段lua脚本
// 释放锁涉及到两条指令,这两条指令不是原子性的
// 需要用到redis的lua脚本支持特性,redis执行lua脚本是原子性的
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
这种方式有几大要点:
- 一定要用SET key value NX PX milliseconds 命令,如果不用,先设置了值,在设置时间,并不是原子操作,则会可能在设置过期时间之前宕机,造成死锁(key永久存在)
- value要具有唯一性,这个是为了在解锁的时候,需要验证value是和加锁的一致才删除key
- 避免了一种情况:假设A获取了锁,过期时间30s,此时35s之后,锁已经自动释放了,A去释放锁,但是此时可能B获取了锁。A客户端就不能删除B的锁了。
- 假如超过30s都还没有完成业务逻辑的情况下,key会过期,其他线程有可能会获取到锁。这样一来的话,第一个线程还没执行完业务逻辑,第二个线程进来了也会出现线程安全问题。
Redisson是一个企业级的开源Redis Client,也提供了分布式锁的支持,推荐使用。
用起来非常简单,实例代码如下:
Config config = new Config();
config.useClusterServers()
.addNodeAddress("redis://192.168.31.101:7001")
.addNodeAddress("redis://192.168.31.101:7002")
.addNodeAddress("redis://192.168.31.101:7003")
.addNodeAddress("redis://192.168.31.102:7001")
.addNodeAddress("redis://192.168.31.102:7002")
.addNodeAddress("redis://192.168.31.102:7003");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("anyLock");
lock.lock();
lock.unlock();
就是这么简单,我们只需要通过它的api中的lock和unlock即可完成分布式锁,他帮我们考虑了很多细节:
- redisson所有指令都通过lua脚本执行,redis支持lua脚本原子性执行
- redisson设置一个key的默认过期时间为30s,如果某个客户端持有一个锁超过了30s怎么办?
- redisson中有一个watchdog(类似Zk节点上的事件监听)的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔10秒帮你把key的超时时间设为30s
- 这样的话,就算一直持有锁也不会出现key过期了,其他线程获取到锁的问题了。
- redisson的“看门狗”逻辑保证了没有死锁发生。
- (如果机器宕机了,看门狗也就没了。此时就不会延长key的过期时间,到了30s之后就会自动过期了,其他线程可以获取到锁)
基于zookeeper实现分布式锁
Zookeeper是一种提供配置管理、分布式协同以及命名的中心化服务。
zk的模型是这样的:zk包含一系列的节点,叫做znode,就好像文件系统一样每个znode表示一个目录,然后znode有一些特性:
-
有序节点:假如当前有一个父节点为/lock,我们可以在这个父节点下面创建子节点;
-
zookeeper提供了一个可选的有序特性,例如我们可以创建子节点“/lock/node-”并且指明有序,那么zookeeper在生成子节点时会根据当前的子节点数量自动添加整数序号
-
也就是说,如果是第一个创建的子节点,那么生成的子节点为/lock/node-0000000000,下一个节点则为/lock/node-0000000001,依次类推。
-
临时节点:客户端可以建立一个临时节点,在会话结束或者会话超时后,zookeeper会自动删除该节点。
-
事件监听(类似redis的WatchDog):在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,zookeeper会通知客户端。当前zookeeper有如下四种事件:
节点创建
节点删除
节点数据修改
子节点变更
基于以上的特性,zk分布式锁的实现如下: -
使用zk的临时顺序节点,每个线程获取锁就是在zk创建一个临时顺序节点
-
创建节点成功后,再判断当前线程创建的节点是否是创建节点目录下所有的节点的序号最小的节点
-
如果是最小的节点,则认为获取锁成功。如果不是,则对节点序号的前一个节点添加一个事件监听。
-
如果锁释放了,会唤醒下一个序号的节点,然后重新判断是否自己的节点序号是最小。
总结
对于redis的分布式锁而言,缺点如下:
- 获取锁的方式简单粗暴,获取不到锁直接不断尝试获取锁,比较消耗性能。
- redis的设计定位决定了它的数据并不是强一致性的。
- 但是,redis实现分布式锁普及率很高,而且大部分情况下都不会遇到所谓的复杂极端场景。最重要的一点是redis的性能很高,可以支撑高并发的获取、释放锁操作。
对于zk的分布式锁而言
- zookeeper天生设计定位就是分布式协调,强一致性。锁的模型健壮、简单易用、适合做分布式锁。
- 如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小
- 如果有较多的客户端频繁的申请加锁、释放锁,对于zk集群的压力会比较大。
- 推荐高可用大型分布式系统推荐zk分布式锁。
最后个人觉的最重要的一点:
无论是Redis分布式锁还是zk分布式锁,设计思路来源于JUC包里的AQS设计思想,都是在加锁时锁定头结点(最小节点),解锁时删除头结点,WatchDog或者WatchNode机制都是模拟双链表,将下一个最小的节点设置为头结点,再次抢锁。