Redis加锁超时怎么办?

近日遇到了一个分布锁线上问题,导致用户获取锁一直失败,被阻拦提单近2H。 bug原因是Redis setnx获取锁超时,但实际写入成功,出现超时后并没有释放锁。由于锁维度是userId维度,导致用户再次提单一直无法获取锁,提单一直失败,客服同学说:用户情绪很激动,骂骂咧咧的。

为了避免锁超时导致并发问题,系统设置的超时时间过长------2小时。这导致用户2个小时都无法提单。由于系统日订单量在千万,即便超时比例非常低,每天也会有10单以上。用户找客服投诉的意愿十分强烈,并且态度很差。

在解决问题前,先回顾下我们分布式锁的技术实现方案

实现方案

  • 加锁命令: redis setnx 命令在key存在时返回false,key不存在返回成功。由于redis命令的原子性,可保证多个只有1个客户端可成功。
  • 解锁命令:del key 删除key,就是释放锁

锁漏释放问题

加锁或解锁异常的情况下,可能出现漏被漏释放。 如果未设置超时时间,那么就会导致锁一直未释放,像我们的提单的场景,如果用户一直无法申请锁,一直提单,用户绝对会有打人的冲动。

超时时间设置长短由业务场景决定,业务耗时短,超时就设置短一些,如果持锁时间长,锁超时设置长一些。

设置超时时间的命令是 EXPIRE KEY timeout 。由于redis并未提供原子命令同时 setnx, expire 。所以要使用lua脚本 改进setnx 命令。(建议公司Redis Client统一提供这个原子命令)

js 复制代码
if (redis.call('setnx',KEYS[1],ARGV[1]) < 1) then 
    return 0; 
end; 
redis.call('EXPIRE',KEYS[1],tonumber(ARGV[2])); 
return 1;

锁误释放问题

为什么会出现锁误释放呢?

假如客户端1持锁时间超过了锁超时时间,redis key被过期删除。然后客户端2申请锁成功。而客户端1 在执行完成后,释放锁。此时会导致 客户端2 申请的锁被客户端1 释放。 如图

sequenceDiagram Client1 ->> Redis : setnx 申请锁 Client1->> Client1: 执行业务逻辑超时 Redis ->> Redis: 锁过期被删除 Client2 ->> Redis:setnx 申请锁成功 Client1->> Redis:释放锁。 note over Redis: Client 1误释放了Client申请的锁,导致Client3 可申请锁成功,未起到防并发的目的 Client3 ->> Redis: setnx 抢占锁成功。

Client 1误释放了Client2 申请的锁,导致Client3 可申请锁成功,未起到防并发的目的。由此可见,删除锁的时候不能盲目删除。删除前应该保证是自己申请的锁,才能释放。

如何实现呢?首先setnx 需要保存value。 value中记录了客户端信息,可以使用毫秒级时间戳、UUID或者 机器ip+线程名称等。(毫秒级时间戳足够用了,一般业务场景分布式锁争抢没那么严重)。

删除操作需要实现 Compare AND DELETE (CAD)原子命令。 redis没有提供,需要使用LUA脚本实现

js 复制代码
if(redis.call('GET',KEYS[1]) ~= false ) then
        local v = redis.call('GET', KEYS[1]);
        if(v~= KEYS[2]) then
            return -1;
        end;
            local res = redis.call('DEL', KEYS[1]);
            if(res==1) then
                return 1;
           else
                return -2;
           end;

end;

return 0;

使用redis-cli加载lua脚本:

js 复制代码
redis-cli  script load "$(cat cad.lua)"

执行lua脚本,指定参数参数,传入KEY、Value

EVALSHA 304be2df131dda36b189882adef40856da7b0259 2 KEY VALUE

返回值说明:

yaml 复制代码
 1:  值相等,可以删除,且删除成功
  0: 当 key1 不存在时
  -1: value1 和key1对应的value不相等
   -2: value1 和key1对应的value相等,但是DEL执行失败。因为redis lua原子化执行,之前已经判断key1存在,所以这种场景几乎不可能,但预留下

Value还有其他更重要的用途

分布式锁为什么会存Value,如果只看到持有锁超时场景,防止误删除是不够的。

回到一开始的问题,如果setnx阶段出现了超时,客户端是不清楚锁是否写入成功。需要重试一下,才能确认。(如果不重试就会出现锁漏释放,导致后续一直无法获取锁。)

超时后,可以get查询一下锁是否存在,如果存在就说明刚才加锁成功了吗?并不能,有可能是其他Client加的锁。此时就需要判断value是否一致,如果一致说明是自己获取成功的,否则说明是其他Client获取成功的。

sequenceDiagram Client1 ->> Redis : setnx key value1 获取锁 Redis ->> Client1 : 获取锁超时 Client1 ->> Redis: GET KEY NOTE OVER Client1: 防止超时,GET也要重试 Client1 ->> Client1: value一致则获取锁成功,否则获取锁失败。

总结

redis 加锁和释放锁都需要设置value,为了防止漏释放锁,更是为了保证加锁超时后,能查询确认上一次加锁是否成功。

到现在,我们线上的问题解决方案已经出来了,我们需要在加锁超时后,重新GET 确认是否加锁成功。

相关推荐
Hello.Reader3 小时前
Redis 延迟监控深度指南
数据库·redis·缓存
ybq195133454313 小时前
Redis-主从复制-分布式系统
java·数据库·redis
好奇的菜鸟6 小时前
如何在IntelliJ IDEA中设置数据库连接全局共享
java·数据库·intellij-idea
tan180°6 小时前
MySQL表的操作(3)
linux·数据库·c++·vscode·后端·mysql
满昕欢喜6 小时前
SQL Server从入门到项目实践(超值版)读书笔记 20
数据库·sql·sqlserver
优创学社27 小时前
基于springboot的社区生鲜团购系统
java·spring boot·后端
why技术7 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
幽络源小助理7 小时前
SpringBoot基于Mysql的商业辅助决策系统设计与实现
java·vue.js·spring boot·后端·mysql·spring
Hello.Reader8 小时前
Redis 延迟排查与优化全攻略
数据库·redis·缓存
ai小鬼头8 小时前
AIStarter如何助力用户与创作者?Stable Diffusion一键管理教程!
后端·架构·github