Redis 分布式锁

版权归原作者所有。 本文最后更新于:2023年12月5日 凌晨

Redis 分布式锁

分布式锁引入

分布式应用就会时长遇到并发问题

client 两个副本想要对 key操作, 副本a 进行get 操作 副本b进行set操作

因为对于这种操作它不是原子性的(原子性:不会被线程调度机制打断的操作,这种操作一旦开始,就会一直运行到结束,中间不会任何有任何线程的切换)

这个时候就要用redis分布式锁来限制程序的并发执行。

对于redis实现分布式锁,逻辑是 在redis里面占一个“坑”,谁先来谁占用,再来的发现有萝卜了,则放弃或者等待,

占坑一般使用setnx(不存在则设置),用完了之后 使用 del 坑

死锁

但是如果这样设置,setnx之后,业务流程执行异常了退出了,导致没有执行del,那这个锁成为死锁了,防止死锁的情况出现,一般给锁都加入一个过期时间,例如5s,这样即使出现异常5s后也会解锁。
上面还是有问题,如果加锁后,程序退出或者服务器断电等问题,导致expire没有执行,这样过期时间没有设置,也会导致死锁发生。

死锁解决

出现上面问题都是因为setnx和expire两个指令操作不是原子性的,在redis2.8之前各种三方实现lib出来,但是是实现都很复杂,对于redis2.8作者直接在set执行的扩展属性,彻底解决了各种三方乱想,

解决命令

set k1 v1 ex 5 nx
// do somthing
del k1

超时问题

在加锁设置超时的时间内没有执行完业务流程,导致时间过期,出现分布式锁在不改被释放时,释放的问题,则为超时问题
当临界区的代码逻辑执行,第一个拿到锁的业务流程没有执行完,锁释放,第二个拿到锁之后执行,但是第二个执行开始,第一个业务流程没有拿到正确的结果,导致两个业务无法正常串行执行,从而引发问题。
所以在设置分布式锁流程尽量不要设置太长时间的业务流程,以免出现该问题,再就是如果真出现了需要人工介入处理。

一个稍微安全的处理方式

在加锁时,锁定的值设置为一个随机值,然后在释放锁时进行进行先比对该值是否是需要解锁的值,然后再 解锁该key

Lucas详细说明补充

当第一个锁设置为一个随机值,然后超时了,第二个获取到锁了,然后第二个执行中, 第一个执行完了,开始释放锁,这个时候去比对随机值发现随机值不是当时设置的那个值,所以就不需要删除该锁,但是对于没有设置随机值,这个时候第二个拿到的锁就被第一个释放了,导致出现问题。
但是先比较再删除锁,这个指令需要两个协作进行操作,不能保证原子性,这个时候就可以借助lua脚本的保证多个执行操作的原子性进行执行。

lua 脚本代码

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

可重入性

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

例如Java的ReentrantLock 就是可重入锁

Redis分布式锁支持可重入,需要配合ThreadLocal实现,变量存储用来计数

ThreadLocal<Map<String, Integer>>

key, 进入次数

加锁时,如果不存在,则创建hashmap 放入tl中,key作为map key,value是1, 如果存在则,通过key获取当前次数 +1 ,再存入,解锁时 ,-1, 当计数-1=0时,则删除 tl中的数据,防止内存泄露

对于Redis分布式锁可重入性,不是很推荐

锁冲突处理

客户端在请求加锁时没加成功怎么办?

一般有以下三种策略:

1、直接抛出异常,通知用户稍后重试!(用的最多)

让用户手动重试,或者提高体验,前端对请求重试,本质上就是放弃该请求加锁

2、sleep一会,然后再重试。

sleep会阻塞当前消息处理线程,如果队列里面消息过多,sleep并不是很适合,还有死锁情况,会导致后面消息永远得不到处理,线程也会被堵死

3、将请求转移到延时队列,过一会再试。

该方式比较适合异步处理,放入一个延迟队列过会再试


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明蚁点博客出处!