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 的长时间失联,锁被释放问题