redis分布式锁

文章详细讨论了Redis中利用SETEX和NX命令实现分布式锁的机制,以及由此带来的问题,如锁过期、误删和线程安全问题。提出了使用lua脚本来保证原子性操作和Redisson框架进行锁续命以增强安全性。进一步引入RedLock,通过多主节点实现更高的可用性,但同时也指出了RedLock存在的问题,如宕机重启后的锁冲突和效率低下。
摘要由CSDN通过智能技术生成

分布式锁特性

redis 原生命令 SET的扩展命令(SET EX PX NX)

命令细节

SET key value[EX seconds][PX milliseconds][NX|XX]

  • NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取。
  • EX seconds :设定key的过期时间,时间单位是秒。
  • PX milliseconds: 设定key的过期时间,单位为毫秒
  • XX: 仅当key存在时设置值

code

if(jedis.set(key_resource_id, lock_value, "NX", "EX", 100s) == 1){ //加锁   锁释放了 任务还没有走完
    try {
        do something  //业务处理
    }catch(){
  }
  finally {
       jedis.del(key_resource_id); //释放锁  // 机器异常了 锁没有释放
    }
}

缺点:

  • 锁过期释放了,业务还没执行完。假设线程a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完。但是,这时候锁已经过期了,此时线程b又请求过来。显然线程b就可以获得锁成功,也开始执行临界区的代码。那么问题就来了,临界区的业务代码都不是严格串行执行的啦。
  • 锁被别的线程误删。假设线程a执行完后,去释放锁。但是它不知道当前的锁可能是线程b持有的(线程a去释放锁时,有可能过期时间已经到了,此时线程b进来占有了锁)。那线程a就把线程b的锁释放掉了,但是线程b临界区业务代码可能都还没执行完呢。
  • 加锁的机器还没有同步到副本上的时候, 宕机了,则锁丢失了

线程A锁提前释放

线程AB公用同一个锁名字 A的锁被B删除

Lua脚本

保证SETNX + EXPIRE两条指令的原子性

redis 原生SET的扩展命令 锁的val唯一 => 解决 锁被其他线程释放问题

code

if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1){ //加锁
    try {
        do something  //业务处理
    }catch(){
  }
  finally {
       //判断是不是当前线程加的锁,是才释放
       if (uni_request_id.equals(jedis.get(key_resource_id))) {
        jedis.del(lockKey); //释放锁
        }
    }
}
Lua 替代删除部分
if redis.call('get',KEYS[1]) == ARGV[1] then 
   return redis.call('del',KEYS[1]) 
else
   return 0
end;

Redisson框架 : 锁续命 + 锁的val唯一 => 解决自身业务没有执行完被释放的问题 + 锁被其他线程释放问题 (val = uuid+threadid)

原理: thread1 加锁成功 通过子线程续命 ; thread2 加锁失败 通过自旋锁等待加锁

问题:集群环境存在问题

如果线程一在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以获取同个key的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。

Redisson框架 + RedLock : 锁续命 + 锁的val唯一 + 集群相对安全(使用主从依旧存在问题) => 解决 自身业务没有执行完被释放的问题 + 锁被其他线程释放问题 + 锁的master宕机

原理

  • 部署多个Redis master,以保证它们不会同时宕掉。并且这些master节点是完全相互独立的,相互之间不存在数据同步。

同时,需要确保在这多个master实例上,是与在Redis单实例,使用相同方法来获取和释放锁。

redlock 过程:

  • 获取当前时间,以毫秒为单位。
  • 按顺序向5个master节点请求加锁。客户端设置网络连接和响应超时时间,并且超时时间要小于锁的失效时间。(假设锁自动失效时间为10秒,则超时时间一般在5-50毫秒之间,我们就假设超时时间是50ms吧)。如果超时,跳过该master节点,尽快去尝试下一个master节点。
  • 客户端使用当前时间减去开始获取锁时间(即步骤1记录的时间),得到获取锁使用的时间。当且仅当超过一半(N/2+1,这里是5/2+1=3个节点)的Redis master节点都获得锁,并且使用的时间小于锁失效时间时,锁才算获取成功。(如上图,10s> 30ms+40ms+50ms+4m0s+50ms)
  • 如果取到了锁,key的真正有效时间就变啦,需要减去获取锁所使用的时间。
  • 如果获取锁失败(没有在至少N/2+1个master实例取到锁或者获取锁时间已经超过了有效时间),客户端要在所有的master节点上解锁(即便有些master节点根本就没有加锁成功,也需要解锁,以防止有些漏网之鱼)。

存在问题:

  • 宕机重启之后,2个客户端拿到同一把锁。 假设5个节点是A, B, C, D, E,客户端1在A, B, C上面拿到锁,D, E没有拿到锁,客户端1拿锁成功。 此时,C挂了重启,C上面锁的数据丢失(假设机器断电,数据还没来得及刷盘;或者C上面的主节点挂了,从节点未同步)。

客户端2去取锁,从C, D, E 3个节点拿到锁,A, B没有拿到(还被客户端1持有),客户端2也超过多数派,也会拿到锁。 解决方案- 延迟重启;但是由于时钟跳变的因素,导致延迟重启时效(无法解决该问题);

  • 脑裂问题:就是多个客户端同时竞争同一把锁,最后全部失败。 比如有节点1、2、3、4、5,A、B、C同时竞争锁,A获得1、2,B获得3、4,C获得5,最后ABC都没有成功获得锁,没有获得半数以上的锁。

官方的建议是尽量同时并发的向所有节点发送获取锁命令。客户端取得大部分Redis实例锁所花费的时间越短,脑裂出现的概率就会越低。 需要强调,当客户端从大多数Redis实例获取锁失败时,应该尽快地释放(部分)已经成功取到的锁,方便别的客户端去获取锁,假如释放锁失败了,就只能等待锁超时释放了

  • 效率低,主节点越多,获取锁的时间越长;

参见: 七种方案!探讨Redis分布式锁的正确使用姿势 - 掘金

参见: Redlock(Redis分布式锁)可能存在的问题_51CTO博客_springboot redis分布式锁

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值