目录
应用场景:
分布式锁是为了保证同一时刻只有一台机器的一个线程执行某段代码。分布式锁的目的如下
-
解决业务层幂等性
-
解决 MQ 消费端多次接受同一消息
-
确保串行|隔离级别
-
多台机器同时执行定时任务
最近在工作中遇到了一些问题,上游重复调用下游接口下发数据导致数据重复,需要用redis锁防重,线程获取不到锁时直接提示给上游已经下发过数据。
redis分布式锁:
主要实现是调用redis的SETNX命令(set if not exists),如果返回1,则获得资源并加锁;如果返回0,则没有获得锁,锁被其他资源占用。需要注意的是:
- 必须设置过期时间,防止获得锁的服务器宕机后产生死锁
- 添加redis数据必须与设置过期时间在同一个命令中,不能分开写,因为redis只保证单条命令的原子性,防止set完数据后还没设置过期时间就宕机产生死锁
- value应该是客户端生成的唯一的字符串,解锁时进行判断,防止释放了别人的锁。(例:客户端1在执行释放锁之前,锁已经过期自动删除,此时客户端2拿到了锁,客户端1执行del操作就会释放客户端2的锁)
- 释放锁的操作必须使用Lua脚本来实现。释放锁其实包含三步操作:GET、判断和DEL,用Lua脚本来实现能保证这三步的原子性
-
获得锁:
SET lock_key random_value NX PX 5000 (SET key value [NX|XX] [EX|PX] seconds )
- NX – 只有键key不存在的时候才会设置key的值
- XX – 只有键key存在的时候才会设置key的值
- EX seconds – 设置键key的过期时间,单位时秒
- PX milliseconds – 设置键key的过期时间,单位时毫秒
-
释放锁:
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end
redis分布式锁存在一些问题:1、锁时间的设置困难,太短可能过早的释放锁,造成数据安全问题。太长的话,如果客户端挂掉会长时间无法释放锁,导致其他客户端锁请求阻塞或者失败,因此需要程序员有丰富的经验;2、为了保证redis的高可用必然要搭建集群,但redis主从同步会有时间间隔,如果一个客户端已经从主节点获得了锁,一旦主节点挂掉或者网络抖动导致切换到从节点后,就可能有另一个客户端重复获得锁。那为什么还广泛使用redis分布式锁呢?我们常用 N 个9 来量化可用性,只要能保证高可用性效果就达到了。
zookeeper分布式锁
锁服务可以分为两类
- 独占锁,就是所有试图来获取这个锁的客户端,最终只有一个可以成功获得这把锁。通常的做法是把 zk 上的一个 znode 看作是一把锁,通过 create znode 的方式来实现。所有客户端都去创建节点,最终成功创建的那个客户端也即拥有了这把锁。
- 时序锁,就是所有试图来获取这个锁的客户端,最终都是会被安排执行,只是有个全局时序了。如果有客户端争抢一个zk分布式锁,大家都是上来直接创建一个锁节点下的一个接一个的临时顺序节点,如果自己不是第一个节点,就对自己上一个节点加监听器,只要上一个节点释放锁,自己就排到前面去了,相当于是一个排队机制。而且用临时顺序节点的另外一个用意就是,如果某个客户端创建临时顺序节点之后,不小心自己宕机了也没关系,zk感知到那个客户端宕机,会自动删除对应的临时顺序节点,相当于自动释放锁,或者是自动取消自己的排队。
zookeeper分布式锁具体实现方式有多种:比如
- 创建临时节点,谁创建成功谁就获得锁,创建不成功的就阻塞并注册监听,当节点释放后再尝试去创建节点(比如创建不成功就new CountDownLatch(1),并调用CountDownLatch.await()方法阻塞并注册监听,监听到节点删除就CountDownLatch.countDown()唤醒线程,继续尝试获取锁)
- 创建顺序临时节点,排序判断谁创建的子节点序号最小谁就获得锁,其余线程找到比自己小的节点,对其调用exist()方法并注册事件监听器。当监听的节点删除时,再次判断自己创建的节点是否是序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个。
参考文章:高可用的分布式锁如何设计