Redis:分布式锁

一、定义

分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”,当别的进程也要来占时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。

二、单个实例的Redis锁

1、指令

//这里的冒号:就是一个普通的字符,没特别含义,它可以是任意其它字符,不要误解
//将竞争锁和设置超时绑定成一个原子操作 避免死锁
> set lock:book 1 ex 10 nx
> dosomething
> del lock:book

2、超时问题

(1) 问题描述

Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。因为这时候锁过期了,第二个线程重新持有了这把锁,但是紧接着第一个线程执行完了业务逻辑,就把锁给释放了,第三个线程就会在第二个线程逻辑执行完之间拿到了锁。这就产生了数据竞争问题

(2) 问题复现
//tty1:第一个线程抢到锁
127.0.0.1:6379> set lock:book 1 ex 10 nx
OK
//tty2:锁过期后第二个线程抢到锁
127.0.0.1:6379> set lock:book 1 ex 10 nx
(nil)
127.0.0.1:6379> set lock:book 1 ex 10 nx
OK
//tty1:紧接着第一个线程删除锁
127.0.0.1:6379> del lock:book
(integer) 1
//tty3:第三个线程抢到锁
127.0.0.1:6379> set lock:book 1 ex 10 nx
OK
(3) 问题解决
第一种方式

Redis分布式锁尽量不要用于较长时间的任务。如果真的偶尔出现了,数据出现的小波错乱可能需要人工介入解决。

第二种方式

更加安全的方案是为 set 指令的 value 参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后再删除 key。
但是匹配 value 和删除 key 不是一个原子操作,Redis 也没有提供类似于 delifequals 这样的指令,这就需要使用 Lua 脚本来处理了,因为 Lua 脚本可以保证连续多个指令的原子性执行。

tag = random.nextint() # 随机数
if redis.set(key, tag, nx=True, ex=5):
do_something()
redis.delifequals(key, tag) # 假象的 delifequals 指令

//Lua script
# delifequals
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])

示例代码

3. Redis锁的可重入问题

(1) 可重入锁的定义

可重入性是指线程在持有锁的情况下再次请求加锁,如果一个锁支持同一个线程的多次加锁,那么这个锁就是可重入的。

(2) Redis分布式锁支持可重入性

redis分布式锁是不支持可冲入性的,如何做才能支持呢?
通过记录锁的引用计数实现,存在引用计数,直接返回,如果不存在,执行lock逻辑。还需要考虑内存锁计数的过期时间。

(3) 建议

不建议使用可重入锁,否则客户端逻辑代码复杂,并且通过调整客户端逻辑,可以避免使用可重入锁。

三、Redis集群的分布式锁

1. 官网地址

2. Redis集群分布式锁到底安全吗

基于Redis的分布式锁到底安全吗(上)?
基于Redis的分布式锁到底安全吗(下)?

3. Redis集群分布式锁使用

(1) 为什么不能使用单点redis分布式锁呢?

在这里插入图片描述
单点reids一旦崩掉,将没有服务可用,没有任何高可用可言,但是采用redis主从架构一样有问题。

比如在 Sentinel 集群中,主节点挂掉时,从节点会取而代之,客户端上却并没有明显感知。原先第一个客户端在主节点中申请成功了一把锁,但是这把锁还没有来得及同步到从节点,主节点突然挂掉了。然后从节点变成了主节点,这个新的节点内部没有这个锁,所以当另一个客户端过来请求加锁时,立即就批准了。这样就会导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生。

不过这种不安全也仅仅是在主从发生 failover 的情况下才会产生,而且持续时间极短,业务系统多数情况下可以容忍。

(2) 使用条件

redis集群分布式锁采用Redlock算法, 为了使用 Redlock,需要提供多个 Redis 实例,这些实例之前相互独立没有主从关系。同很多分布式算法一样,redlock 也使用「大多数机制」。

加锁时,它会向过半节点发送 set(key, value, nx=True, ex=xxx) 指令,只要过半节点 set成功,那就认为加锁成功。释放锁时,需要向所有节点发送 del 指令。不过 Redlock 算法还需要考虑出错重试、时钟漂移等很多细节问题,同时因为 Redlock 需要向多个节点进行读写,意味着相比单实例 Redis 性能会下降一些

(3) ReadLock算法使用安全条件

详细请参考官网

  • 进行多物理机之间时钟同步
  1. 避免情况: 1) 系统管理员手动修改了时钟. 2) 从NTP服务收到了一个大的时钟更新事件
  2. 正确同步方式: 1) 不要手动修改时钟. 2) 使用一个不会进行“跳跃”式调整系统时钟的ntpd程序(可能是通过恰当的配置),对于时钟的修改通过多次微小的调整来完成。
  • 当客户端无法取到锁时,应该在一个随机延迟后重试(这个时间必须大于从大多数Redis实例成功获取锁使用的时间),防止多个客户端在同时抢夺同一资源的锁(这样会导致脑裂,没有人会取到锁)。同样,客户端取得大部分Redis实例锁所花费的时间越短,脑裂出现的概率就会越低(必要的重试),所以,理想情况一下,客户端应该同时(并发地)向所有Redis发送SET命令。
  • 当客户端从大多数Redis实例获取锁失败时,应该尽快地释放(部分)已经成功取到的锁,这样其他的客户端就不必非得等到锁过完“有效时间”才能取到
  • 需要在锁的有效期内进行操作,使用的时间接近或者已经大于失效时间,客户端将认为锁是失效的锁,并且将释放掉已经获取到的锁
  • 当一个redis节点重启后,只要它不参与到任意当前活动的锁,没有被当做“当前存活”节点被客户端重新获取到,算法的安全性仍然是有保障的。为了达到这种效果,我们只需要将新的redis实例,在一个TTL时间内,对客户端不可用即可,在这个时间内,所有客户端锁将被失效或者自动释放.
  • 如果你的工作可以拆分为许多小步骤,可以将有效时间设置的小一些,使用锁的一些扩展机制。在工作进行的过程中,当发现锁剩下的有效时间很短时,可以再次向redis的所有实例发送一个Lua脚本,让key的有效时间延长一点(前提还是key存在并且value是之前设置的value)。客户端扩展TTL时必须像首次取得锁一样在大多数实例上扩展成功才算再次取到锁,并且是在有效时间内再次取到锁(算法和获取锁是非常相似的)。
  • 对于客户端可能出现GC现象导致锁过期,write实现可能需要采用CAS思想(借助lua脚本实现原子操作)
  1. 先设置X.currlock = token
  2. 读出资源X(包括它的值和附带的X.currlock),按照”write-if-currlock == token”的逻辑,修改资源X的值。
(4) 使用场景

1. 如果你很在乎高可用性,希望挂了一台 redis 完全不受影响,那就应该考虑 redlock。不过代价也是有的,需要更多的 redis 实例,性能也下降了,代码上还需要引入额外的library,运维上也需要特殊对待,这些都是需要考虑的成本,使用前请再三斟酌。
2. 如果你很在乎数据安全和正确性,可以考虑zookeeper分布式锁

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值