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) 1127.0.0.1:6379> EXPIRE lock 10 // 10s后主动过期(integer) 1//这种状况不是原子操作,任然会产生死锁问题
// 一条命令保障原子性执行127.0.0.1:6379> SET lock 1 EX 10 NXOK
这种状况仍会产生问题:
- 锁过期:客户端 1 操作共享资源耗时太久,导致锁被主动开释,之后被客户端 2 持有
- 开释他人的锁:客户端 1 操作共享资源实现后,却又开释了客户端 2 的锁
解决方案
// 锁的VALUE设置为UUID127.0.0.1:6379> SET lock $uuid EX 20 NXOK
开释锁时,应用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 的长时间失联,锁被开释问题