redis分布式锁

// 一条命令保证原子性执行
127.0.0.1:6379> SET lock 1 EX 10 NX
OK

设想这样一种场景:

  1. 客户端 1 加锁成功,开始操作共享资源
  2. 客户端 1 操作共享资源的时间,「超过」了锁的过期时间,锁被「自动释放」
  3. 客户端 2 加锁成功,开始操作共享资源
  4. 客户端 1 操作共享资源完成,释放锁(但释放的是客户端 2 的锁)

这里存在两个严重的问题:

  1. 锁过期:客户端 1 操作共享资源耗时太久,导致锁被自动释放,之后被客户端 2 持有
  2. 释放别人的锁:客户端 1 操作共享资源完成后,却又释放了客户端 2 的锁

锁被别人释放怎么办?

客户端在加锁时,可以设置一个只有自己知道的「唯一标识」进去。

这个唯一标识,可以是自己的线程ID,也可以是一个UUID(随机且唯一)

// 锁的VALUE设置为UUID
127.0.0.1:6379> SET lock $uuid EX 20 NX
OK

在释放锁时,要先判断这把锁是否还归自己持有,伪代码可以这么写:

// 锁是自己的,才释放
if redis.get("lock") == $uuid:
    redis.del("lock")

这里释放锁使用的是 GET + DEL 两条命令,可能会存在原子性问题:

  1. 客户端 1 执行 GET,判断锁是自己的(然后客户端 1 加的锁在 GET 之后过期了)
  2. 客户端 2 执行 SET 命令,强制获取到锁(虽然发生概率比较低,但我们需要严谨地考虑锁的安全性问题)
  3. 客户端 1 执行 DEL,却释放了客户端 2 的锁

所以,这两个命令还是必须要原子执行才行。

使用lua脚本解决原子性问题,因为 Redis 处理每一个请求都是「单线程」执行的,在执行一个 Lua 脚本时,其它请求必须等待,直到这个 Lua 脚本处理完成,这样一来,GET + DEL 之间就不会插入其它命令了。

安全释放锁的 Lua 脚本如下:

// 判断锁是自己的,才释放
if redis.call("GET",KEYS[1]) == ARGV[1]
then
    return redis.call("DEL",KEYS[1])
else
    return 0
end

使用redis分布式锁严谨的流程

  1. 加锁:SET lock_key $unique_id EX $expire_time NX
  2. 操作共享资源
  3. 释放锁:Lua 脚本,先 GET 判断锁是否归属自己,再 DEL 释放锁

锁过期时间不好评估怎么办?

加锁时,先设置一个过期时间,然后我们开启一个「守护线程」,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行「续期」,重新设置过期时间。

集群模式下,分布式锁问题

客户端在主库上执行 SET 命令,加锁成功,此时,主库发生异常宕机,SET 命令还未同步到从库上(主从复制是异步的),从库被哨兵提升为新主库,这个锁在新的主库上,丢失了!

解决方案:redlock(红锁) (至少 5 个实例)

Redlock 整体流程:

  1. 客户端先获取「当前时间戳T1」
  2. 客户端依次向这 5 个 Redis 实例发起加锁请求,且每个请求会设置请求超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁
  3. 如果客户端从 >=3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳T2」,如果 T2 - T1 < 锁的过期时间,此时认为客户端加锁成功,否则认为加锁失败
  4. 加锁成功,去操作共享资源(例如修改 MySQL 某一行,或发起一个 API 请求)
  5. 加锁失败,向「全部节点」发起释放锁请求(Lua 脚本释放锁)

为什么要在多个实例上加锁?

本质上是为了「容错」,部分实例异常宕机,剩下的实例加锁成功,整个锁服务依旧可用。

为什么大多数加锁成功,才算成功?

多个 Redis 实例一起使用,其实就组成了一个「分布式系统」。

在分布式系统中,总会出现「异常节点」,所以在讨论分布式系统问题时,需要考虑异常节点达到多少个,也依旧不会影响整个系统的「正确性」。

这是一个分布式系统「容错」问题,这个问题的结论是:如果存在「故障」节点,只要大多数节点正常,那么整个系统依旧是可以提供正常服务的

为什么加锁成功后,还要计算加锁的累计耗时?

因为操作的是多个 Redis 节点,所以耗时肯定会比操作单个实例耗时更久,而且因为是网络请求,网络情况是复杂的,有可能存在延迟、丢包、超时等情况发生,网络请求越多,异常发生的概率就越大。

所以,即使大多数节点加锁成功,但如果加锁的累计耗时已经「超过」了锁的过期时间,那么此时有些实例上的锁可能已经失效了,这个锁就没有意义了。

为什么释放锁,要操作所有节点?

在某一个 Redis 节点加锁时,可能因为「网络原因」导致加锁失败。

例如,客户端在一个 Redis 实例上加锁成功,但在读取响应结果时,可能因为「网络问题」导致读取失败,那这把锁其实已经在 Redis 上加锁成功了。所以在释放锁时,不管之前有没有加锁成功,都需要释放「所有节点」的锁,以保证清理某些节点上「残留」的锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值