1. SETNX
一般加锁形式, 示意SET if Not eXists, 当 key 不存在时才会去设置它的值, 否则什么也不做
// 客户端 1 申请加锁, 加锁胜利
127.0.0.1:6379> SETNX lock 1
(integer) 1 // 客户端 1,加锁胜利
// 客户端 2 申请加锁, 加锁失败
127.0.0.1:6379> SETNX lock 1
(integer) 0 // 客户端 2,加锁失败
操作实现后再去开释锁
127.0.0.1:6379> DEL lock // 开释锁
(integer) 1
存在缺点:
- 程序处理业务逻辑异样,没及时开释锁
- 过程挂了,没机会开释锁
如何防止死锁
设置过期工夫
127.0.0.1:6379> SETNX lock 1 // 加锁
(integer) 1
127.0.0.1:6379> EXPIRE lock 10 // 10s 后主动过期
(integer) 1
// 这种状况不是原子操作, 任然会产生死锁问题
// 一条命令保障原子性执行
127.0.0.1:6379> SET lock 1 EX 10 NX
OK
这种状况仍会产生问题:
- 锁过期:客户端 1 操作共享资源耗时太久,导致锁被主动开释,之后被客户端 2 持有
- 开释他人的锁:客户端 1 操作共享资源实现后,却又开释了客户端 2 的锁
解决方案
// 锁的 VALUE 设置为 UUID
127.0.0.1:6379> SET lock $uuid EX 20 NX
OK
开释锁时, 应用 lua 脚本判断 lock 键的值是否是本人的 uuid, 如果是则开释
Redis 解决每一个申请是「单线程」执行, 在执行 lua 脚本时, 其余申请必须期待
综上所述, 基于 redis 的分布式锁, 流程应该如下:
- 加锁:SET lock_key $unique_id EX $expire_time NX
- 操作共享资源
- 开释锁:Lua 脚本,先 GET 判断锁是否归属本人,再 DEL 开释锁
解决锁过期问题:
应用 redisson, 加锁时先设置一个过期工夫, 而后开启一个守护线程, 定时去检测锁的生效工夫, 如果锁快过期, 操作共享资源还未完结, 则进行续期, 从新设置过期工夫. 默认过期工夫为 30s, 检测时间为 20s
2.RedLock
在单机 redis 上,setnx 加锁形式齐全够用, 但在主从集群 + 哨兵模式下, 却会产生如下问题:
- 客户端 1 在主库上执行 SET 命令,加锁胜利
- 此时,主库异样宕机,SET 命令还未同步到从库上(主从复制是异步的)
- 从库被哨兵晋升为新主库,这个锁在新的主库上,失落了!
解决方案
不须要部署从库和哨兵实例, 只部署主库, 主库至多安排 5 个实例
redlock 加锁流程:
- 客户端先获取「以后工夫戳 T1」
- 客户端顺次向这 5 个 Redis 实例发动加锁申请,且每个申请会设置超时工夫(毫秒级,要远小于锁的无效工夫),如果某一个实例加锁失败(包含网络超时、锁被其它人持有等各种异常情况),就立刻向下一个 Redis 实例申请加锁
- 如果客户端从 >=3 个(大多数)以上 Redis 实例加锁胜利,则再次获取「以后工夫戳 T2」,4. 如果 T2 – T1 < 锁的过期工夫,此时,认为客户端加锁胜利,否则认为加锁失败
- 加锁胜利,去操作共享资源(例如批改 MySQL 某一行,或发动一个 API 申请)
- 加锁失败,向「全副节点」发动开释锁申请(后面讲到的 Lua 脚本开释锁)
争议
1. 分布式锁无非是两个目标, 一是效率, 防止反复工作, 二是正确性, 避免重大数据谬误或失落问题,
如果为了效率, 能够用单机 redis, 如果为了正确性,redlock 达不到安全性要求
2. 分布式系统会遇到的三个问题:NPC,N(NETWORK DELAY, 网络提早),P(PEOCESS PAUSE) 过程暂停如 GC,C(CLOCK DRIFT)时钟漂移;
GC 导致锁抵触
- 客户端 1 申请锁定节点 A、B、C、D、E
- 客户端 1 的拿到锁后,进入 GC(工夫比拟久)
- 所有 Redis 节点上的锁都过期了
- 客户端 2 获取到了 A、B、C、D、E 上的锁
- 客户端 1 GC 完结,认为胜利获取锁
- 客户端 2 也认为获取到了锁,产生「抵触」
- 不只是 GC, 产生网络提早, 时钟漂移也会导致 redlock 出问题
时钟正确导致锁抵触
1. 客户端 1 获取节点 A、B、C 上的锁,但因为网络问题,无法访问 D 和 E
2. 节点 C 上的时钟「向前跳跃」,导致锁到期
3. 客户端 2 获取节点 C、D、E 上的锁,因为网络问题,无法访问 A 和 B
4. 客户端 1 和 2 当初都置信它们持有了锁(抵触)
5. 不只是时钟跳跃, 解体后立刻重启也会产生相似问题
解决方案,fecing token
1. 客户端在获取锁时,锁服务能够提供一个「递增」的 token
2. 客户端拿着这个 token 去操作共享资源
3. 共享资源能够依据 token 回绝「后来者」的申请
zookeeper 的锁平安吗
1. 客户端 1 创立长期节点 /lock 胜利,拿到了锁
2. 客户端 1 产生长时间 GC
3. 客户端 1 无奈给 Zookeeper 发送心跳,Zookeeper 把长期节点「删除」
4. 客户端 2 创立长期节点 /lock 胜利,拿到了锁
5. 客户端 1 GC 完结,它依然认为本人持有锁(抵触)
Zookeeper 的长处:
不须要思考锁的过期工夫
watch 机制,加锁失败,能够 watch 期待锁开释,实现乐观锁
但它的劣势是:
性能不如 Redis
部署和运维老本高
客户端与 Zookeeper 的长时间失联,锁被开释问题