redis分布式锁

1、什么是分布式锁

分布式锁可以解决分布在不同机器上的不同进程,要同时操作一个共享资源(例如修改数据库的某一行),实现互斥。

2、怎么实现分布式锁

想要实现分布式锁,必须借助一个外部系统,所有进程都去这个系统上申请加锁。而这个外部系统,必须要实现互斥的能力,即两个请求同时进来,只会给一个进程返回成功,另一个返回失败。

这个外部系统,可以是 MySQL,也可以是 Redis、ZooKeeper、Etcd。

3、redis实现分布式锁

3.1.redis如何实现分布式锁的

加锁主要是使用redis中的SETNX命令,这个命令表示SET if Not eXists,即如果 key 不存在,才会设置它的值,否则什么也不做。

127.0.0.1:6379> SETNX lock 1
(integer) 1     // 客户端1,加锁成功
127.0.0.1:6379> SETNX lock 1
(integer) 0     // 客户端2,加锁失败

释放锁直接使用 DEL 命令删除这个 key 即可。

127.0.0.1:6379> DEL lock

以上是基本的实现分布式锁的加锁与释放锁的原理。但这个分布式锁在实现业务逻辑的时候会碰到各种问题,我们基于这些问题进一步的完善这个分布式锁。

问题1:死锁问题

当遇到以下两种情况时:

  1. 程序处理业务逻辑异常,没及时释放锁;

  2. 进程挂了,没机会释放锁。

拿着锁的这个客户端就会一直占用这个锁,而其它客户端就永远拿不到这把锁了,造成死锁问题。

对于情况1,我们的处理方式是通过业务代码try ... catch ... fianlly: redis.del(key)来解决。

对于情况2,我们可以在加锁时,给锁设置有效期

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

这里注意,加锁和给锁设置有效期是两个动作,为了满足这两个动作的原子性,我们用一条redis命令来实现。

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

根据以上情景,我们可以引出两个问题。一个是锁过期时间问题,一个是锁被别人释放问题。

问题2:锁过期时间问题

客户端在拿到锁之后,在操作共享资源时,遇到的场景有可能是很复杂的,例如,程序内部发生异常、网络请求超时等等。因为这些问题不好确定,因此我们很难预估给锁设置的过期时间是多长。如果预估得太短,就会出现上面人家好好的在操作共享资源,结果锁超时释放的问题。而如果预估时间太长,一旦某个持有锁的客户端释放锁失败,那么就会导致所有其它客户端都无法获取锁,从而长时间内无法正常工作。

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

redisson中的Watch Dog 机制就是用来给锁续约的。保证锁是业务执行完释放的,而不是因为超时释放的。

  • leaseTime,即超时释放时间。如果用户没设置,那么默认就会开启看门狗机制。默认设置的超时释放时间为看门狗时间30s。

  • waitTime,在waitTime时间范围内,如果获取锁失败,会等到这个锁的有效期到了不断重试。如果waitTime时间到了,那么就不会重试,直接返回false。

以下是获取锁成功后如何续约的源码:

进入尝试获取锁方法内

进入scheduleExpirationRenewal方法中

进入renewExpiration方法中

这个延时任务执行的内容如下:

unlock方法里的cancel方法:

综上,一个线程首次获得锁后,就会开启每隔10s(leaseTime=30s)的续约任务。直到这个线程unlock时,这个续约任务才会停止。

问题3:锁被别人释放问题

解决方案:客户端在加锁时,给key的value设置为一个只有自己知道的「唯一标识」进去。这个唯一标识可以是uuid,也可以是uuid+线程id。

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

在释放锁时,要先判断这把锁是否还归自己持有。

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

因为GET + DEL 是两条命令,且这两个命令还是必须要原子执行才行。因此我们用Lua 脚本来解决。我们可以把这个逻辑,写成 Lua 脚本,让 Redis 来执行。因为 Redis 处理每一个请求是单线程执行的,在执行一个 Lua 脚本时,其它请求必须等待,直到这个 Lua 脚本处理完成,这样一来,GET + DEL 之间就不会插入其它命令了

总结:在实现redis分布式锁时,

1、实现加锁逻辑,需要设置超时时间、将锁的value设置为唯一标识;

2、操作共享资源时,开启守护线程,定期给锁续期;

3、在释放锁时用lua脚本执行先判断锁是不是自己的再释放锁的两个命令。

参考

Redis、ZooKeeper、Etcd,谁有最好用的分布式锁? (qq.com)

聊聊分布式锁 (qq.com)

分布式锁实现原理与最佳实践 (qq.com) 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值