redis分布式锁的各种实现方案以及问题点分析

redis分布式锁的各种实现方案以及问题点分析

redis的分布式锁暂时发现如下几种实现:

  1. 通过 SETNX, GET, GETSET实现。
  2. 通过 SET key value NX EX milliseconds 实现。
  3. Redisson
  4. Redlock

CAP定理

一致性(Consistency),可用性(Availability), 分区容错性(Partition Tolerance)。
任何一个分布式系统最多同时满足以上两点,所以通常需要牺牲其中一点来最大化满足另两点。
上面几种实现方式各有问题,适合业务的就是最好的。

通过 SETNX, GET, GETSET实现:

因为通过第二种方式可以实现SETNX, GETSET等操作,所以redis正考虑在未来版本弃用SETNX, SETEX, GETSET。 原文(Note: Since the SET command options can replace SETNX, SETEX, PSETEX, GETSET, it is possible that in future versions of Redis these commands will be deprecated and finally removed.)

  1. SETNX key value。
    将key的值设置为value,当且仅当key不存在。 设置成功返回1, 设置失败返回0。
  2. GETSET key value。
    将key的值设置为value,并且将旧的值返回,如果key不存在,返回nil。
  3. GET key。
    获取key的值,如果key不存在,那么返回nil。

锁实现:

# 线程1获取锁  
127.0.0.1:6379> setnx lock:a 1623837618  
# 获取成功
(integer) 1  
# 线程2获取锁  
127.0.0.1:6379> setnx lock:a 1623837618  
# 获取失败
(integer) 0

# 线程1释放锁
127.0.0.1:6379> del lock:a
(integer) 1

这种情况下实现了基本的锁互斥,但是存在一个问题:

  • 出现死锁:如果线程1 出现问题,导致锁没有释放。

为了解决这个问题,有两个解决方案,一个是为锁添加过期时间,一个是将锁的值设置时间戳,抢占的时候判断是否过期。
为锁添加过期时间:

# 线程1获取锁  
127.0.0.1:6379> setnx lock:a 1623837618  
(integer) 1  
# 设置锁最多10秒过期
127.0.0.1:6379> expire lock:a 10  
(integer) 1  
# 线程2获取锁  
127.0.0.1:6379> setnx lock:a 1623837619  
# 获取失败
(integer) 0

# 线程1释放锁
127.0.0.1:6379> del lock:a
(integer) 1

将锁的值设置为时间戳,抢占时判断是否过期:

# 线程1先检查锁是否存在
127.0.0.1:6379> get lock:a   
(nil)  
# 线程2先检查锁是否存在
127.0.0.1:6379> get lock:a   
(nil)  
# 线程1通过getset获取锁, 因为旧值为空,所以获取到锁
127.0.0.1:6379> getset lock:a 1623837618
(nil)
# 线程2通过getset获取锁, 因为返回的时间戳并未过期,所以没有获取到锁
127.0.0.1:6379> getset lock:a 1623837619
"1623837618"

上面两种方式通常情况下都不会导致超时死锁,但是依然存在两个问题。

  1. 锁被抢占:超时时间定多少?有可能线程1任务处理时间过长,导致线程2提前抢占锁。
  2. 锁被误删: 线程1超时处理完任务后,释放了锁,但是锁已被线程2提前抢占到,那么对于线程2,锁被误删了。
  3. 仍然出现死锁: 假设redis主服务器挂掉, 从库转为主库,但是主从只同步了setnx,没有同步expire,那么此时的锁没有设置过期时间,仍然出现死锁。

通过 SET key value NX EX milliseconds 实现:

为了解决问题3 主服务器挂掉,锁不会超时的问题,可以使用SET key value NX EX milliseconds实现,因为这条命令实现了SETNX, EXPIRE的效果,那么在主从同步命令的时候,一定也是作为一条命令同步过去的,所以即使在主服务器执行完这条命令后挂掉,从服务器也不会出现死锁。因为假设这条命令主从同步了,那么从服务器就设置了锁也设置了过期时间,假设这条命令没同步,那么从服务器也就没有锁。
使用SET来设置锁也是redis建议的方式。

# 线程1获取到锁
127.0.0.1:6379> set look:a 1623837618 NX EX 10
OK
# 线程2获取锁失败
127.0.0.1:6379> set look:a 1623837618 NX EX 10
(nil)
# 线程1删除锁
127.0.0.1:6379> del lock:a
(integer) 1

为了解决问题2 锁被误删,可以将值设置为每个线程唯一的随机数或线程id,在释放的时候进行值判断,保证只释放自己线程持有的锁。

if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end

即使存在问题1,那么也能满足很多业务了,同时只要解决了问题1,问题2也就不存在了。
但是如果想解决问题1,只能从设计上考虑,比如线程1获取锁后,启动一个辅助线程,定期的更新锁过期时间,通常(超时时间/3)更新一次,来防止锁被提前释放。

Redisson

Redisson是一个较为广泛应用的开源项目,https://github.com/redisson/redisson
它就是通过不断地更新锁过期时间来续命来解决锁超时问题。
Redssion使用见https://blog.51cto.com/xvjunjie/2428610

不论哪一种实现,都存在一个问题
  • 线程1从主服务器获取锁后,主挂掉了,锁还没来得及同步到从,从切换为主后,这个时候线程2仍能获取倒锁。

Redlock算法

Redlock主要思想是从多个redis主节点获取锁,当获取成功的锁个数大于节点数一半的时候,才算获取成功。
具体Redlock需要执行以下几个步骤:

  1. 获取当前Unix时间,以毫秒为单位
  2. 依次尝试从5个实例,使用相同的key和具有唯一性的value获取锁当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间,这样可以防止在锁获取失败客户端仍然死等。
  3. 客户端使用当前时间减去开始获取锁时间就得到获取锁使用的时间。当且仅当从半数以上的Redis节点取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功
  4. 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间,这个很重要
  5. 如果因为某些原因,获取锁失败(没有在半数以上实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁,无论Redis实例是否加锁成功,因为可能服务端响应消息丢失了但是实际成功了(客户端请求的响应成功了,但是因为网络原因,服务端返回的响应丢失了),毕竟多释放一次也不会有问题

参考

https://redis.io/commands/set
https://www.infoq.cn/article/dvaaj71f4fbqsxmgvdce
https://www.bookstack.cn/read/redis-tutorial/18.md
https://blog.51cto.com/xvjunjie/2428610
https://juejin.cn/post/6844904039218429960

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值