Redis + Lua实现分布式锁(SpringBoot版)

设计思路

既然是实现分布式锁,那肯定得保证多个连接集中请求一个资源的排他性,而redis的单线程特性则很好的满足了这一需求。redis提供的set方法则是满足这一需求的关键,下图是实现redis分布式锁的简单流程,先有个初步的料及。
《Redis + Lua实现分布式锁(SpringBoot版)》

场景分析

下面是set命令的相关用法:

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

可选参数:

  • EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
  • PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
  • NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
  • XX :只在键已经存在时,才对键进行设置操作。

看似简单的实现,实则有很多隐藏的坑,下面我将以几个案例作为分析.

Case1 :

熟悉redis命令的同学可能注意到
setnx这个命令,然后再配合
expire命令是否可以实现一样的效果了?

SETEX user_id 10086
// 此处服务挂掉了,那么user_id将永生
expire user_id

注意,这是2条命令也就意味着这是非原子性操作,当执行到第一条命令SETEX user_id 10086,再准备给这个key设置失效时间时服务突然挂掉了,那么这个key将会永生,其他请求将永远无法获取到这把锁。
所以,我们需要用SET保证其原子性. SET user_id 10086 EX 30 NX,即设置key为user_id,value为10086,并且设置失效时间为30s,如果该key存在则放弃更改。

Case2 :

在我们设置key值的时候一般会尽量保证他的唯一性,比如订单ID,库存ID等。而根据
SET命令的返回结果来判断是否有其他请求强占,貌似value值的设置可有可无,事实真的如此吗?

伪代码:

SET user_id 10086 EX 30 NX
// 处理业务中

//业务处理完毕
DEL user_id

流程图分析:

《Redis + Lua实现分布式锁(SpringBoot版)》

通过上图我们将请求的过程肢解,即分为以下几步:

  1. 三个请求A,B,C同时竞争锁,被请求A抢先获得,其他请求只能不断尝试获取锁(tryLock)
  2. 请求A由于业务比较复杂处理时间已经超时,所以请求B能够获取到锁
  3. 请求A终于完成了自己的业务,这个时候执行了DEL user_id,但是他自己的锁已经失效了,删除的是请求B锁。而请求B的业务此时并未处理完,所以此处就出现了问题!

改进:
为了避免误删除别人的锁,所以我们需要在删除锁的时候需要判断一下这个锁是否是自己的。这个时候我们设置的value就生效了,可以通过value来判断这把锁是否属于自己。 这个value值设置比较随意,只要能做区分就可以了。

伪代码:

SET user_id 10086 EX 30 NX
// 处理业务中

//业务处理完毕
if( (GET user_id) == "XXX" ){
  DEL user_id
}

Case3 :

好吧,终于解决了这一系列坑本以为就要完工 。正在得意回味自己改进的代码时总觉得哪里有点怪怪的,猛地发现这个 GET取值判断和DEL删除并非原子操作。那么接着上面的分析,会出现什么问题呢?

if( (GET user_id) == "XXX" ){ //获取到自己锁后,进行取值判断且判断为真。此时,这把锁恰好失效。
  DEL user_id
}

当程序判通过该锁的值判断发现这把锁是自己加上的,准备DEL。此时该锁恰好失效,而另外一个请求恰好获得key值为user_id的锁。
此时程序执行了了DEL user_id,删除了别人加的锁,尴尬!
所以这段代码并不完美,为了保证查询和删除的原子性操作,需要引入lua脚本支持。

改进:

String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId));

补充

回到Case2中,请求A中的业务处理时长超过了锁的失效时间。对于此类问题,看到很多网上大佬给出的答案是起一个守护线程进行监听key的失效时间,然后在快要失效的时候为期续命。

其实redis对于分布式锁,Redisson有着更好的实现方式。

代码(稍后上传….)

    原文作者:时光沉旧了少年
    原文地址: https://segmentfault.com/a/1190000017042135
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞