近日遇到了一个分布锁线上问题,导致用户获取锁一直失败,被阻拦提单近2H。 bug原因是Redis setnx获取锁超时,但实际写入成功,出现超时后并没有释放锁。由于锁维度是userId维度,导致用户再次提单一直无法获取锁,提单一直失败,客服同学说:用户情绪很激动,骂骂咧咧的。
为了避免锁超时导致并发问题,系统设置的超时时间过长——2小时。这导致用户2个小时都无法提单。由于系统日订单量在千万,即便超时比例非常低,每天也会有10单以上。用户找客服投诉的意愿十分强烈,并且态度很差。
在解决问题前,先回顾下我们分布式锁的技术实现方案
加锁或解锁异常的情况下,可能出现漏被漏释放。 如果未设置超时时间,那么就会导致锁一直未释放,像我们的提单的场景,如果用户一直无法申请锁,一直提单,用户绝对会有打人的冲动。
超时时间设置长短由业务场景决定,业务耗时短,超时就设置短一些,如果持锁时间长,锁超时设置长一些。
设置超时时间的命令是 EXPIRE KEY timeout 。由于redis并未提供原子命令同时 setnx, expire 。所以要使用lua脚本 改进setnx 命令。(建议公司Redis Client统一提供这个原子命令)
if (redis.call('setnx',KEYS[1],ARGV[1]) < 1) then
return 0;
end;
redis.call('EXPIRE',KEYS[1],tonumber(ARGV[2]));
return 1;
有朋友反馈目前Redis Set命令支持同时设置超时时间,无需 再写LUA脚本。我亲自试验了一下,确实如此。 实验Redis版本要求在2.6.12以后。命令和实验截图如下
set key1 value nx ex 10
为什么会出现锁误释放呢?
假如客户端1持锁时间超过了锁超时时间,redis key被过期删除。然后客户端2申请锁成功。而客户端1 在执行完成后,释放锁。此时会导致 客户端2 申请的锁被客户端1 释放。 如图
Client1RedisClient2Client3Client 1误释放了Client申请的锁,导致Client3 可申请锁成功,未起到防并发的目的setnx 申请锁执行业务逻辑超时锁过期被删除setnx 申请锁成功释放锁。setnx 抢占锁成功。Client1RedisClient2Client3
Client 1误释放了Client2 申请的锁,导致Client3 可申请锁成功,未起到防并发的目的。由此可见,删除锁的时候不能盲目删除。删除前应该保证是自己申请的锁,才能释放。
如何实现呢?首先setnx 需要保存value。 value中记录了客户端信息,可以使用毫秒级时间戳、UUID或者 机器ip+线程名称等。(毫秒级时间戳足够用了,一般业务场景分布式锁争抢没那么严重)。