Redis分布式锁
场景:
修改时,需要先把数据读取到计算机内存,在内存中修改后再存回去,而读和写都是非原子操作,因此在并发环境下部分对数据的操作可能会丢失。在单服务器系统中,使用同步锁(本地锁)可以避免线程之间的这种并发问题,但在分布式系统中,需要用分布式锁来保证数据的一致性。因为一台服务器的锁约束不到另一台服务器。
解决方案
- 把锁放到数据库里
- 把锁放到Redis里(常用)
- 把锁放到Zookeeper里
则加锁解锁伪代码如下:
if (setnx(key, 1) == 1){
expire(key, 30)
try {
//TODO 业务逻辑
} finally {
del(key)
}
}
分布式锁可能产生的问题
1、死锁
问题详情:加锁和设置过期时间的动作非原子性。如果 SETNX 成功,在设置锁超时时间时,服务器挂掉、重启或网络问题等,导致 EXPIRE 命令没有执行,锁没有设置超时时间变成死锁。
解决办法:有很多开源代码来解决这个问题,比如使用 lua 脚本。示例:
if (redis.call('setnx', KEYS[1], ARGV[1]) < 1)
then return 0;
end;
redis.call('expire', KEYS[1], tonumber(ARGV[2]));
return 1;
// 使用实例
EVAL "if (redis.call('setnx',KEYS[1],ARGV[1]) < 1) then return 0; end; redis.call('expire',KEYS[1],tonumber(ARGV[2])); return 1;" 1 key value 100
2、超时解锁导致并发(锁过期,线程还没处理完)
如果线程 A 成功获取到了锁,但到了过期时间仍没有执行完,锁过期自动释放,此时线程 B 获取到了锁;那么线程 A 和线程 B 并发执行,这是不被允许的。
解决办法:
- 将过期时间设置足够长,确保代码逻辑在锁释放之前能够执行完成。
- 为获取锁的线程增加守护线程,为将要过期但未释放的锁增加有效时间。
3、锁误解除
问题详情:如果线程 A 成功获取到了锁,但到了过期时间仍没有执行完,锁过期自动释放,此时线程 B 获取到了锁;随后 A 执行完成,使用 DEL 命令来释放锁,那么它实际释放的是线程 B 的锁。
解决办法:用UUID 标识当前线程,在删除时判断锁是否是当前线程持有。