Redis 锁主要利用 Redis 的 SETNX 命令。
伪代码如下:
if (setnx(key, 1) == 1){
expire(key, 30)
try {
//TODO 业务逻辑
} finally {
del(key)
}
}
SETNX成功之后,将要设置锁超时时间的时候,服务器挂掉、重启、网络故障等原因,导致EXPIRE命令没有执行,锁变成死锁。
可以通过lua代码来实现原子性问题
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
线程A设置的锁的时间超时释放之后,线程A仍未执行完毕,正在执行线程B的过程中,A解锁了线程B加的锁
解决办法:在value上加上当前线程加锁的标识,解锁的时候,校验标识。
// 加锁
String uuid = UUID.randomUUID().toString().replaceAll("-","");
SET key uuid NX EX 30
// 解锁
if (redis.call('get', KEYS[1]) == ARGV[1])
then return redis.call('del', KEYS[1])
else return 0
end
上一个图超时解锁带来的并发问题解决如下:
当线程在持有锁的情况下再次请求加锁,如果一个锁支持一个线程多次加锁,那么这个锁就是可重入的。
Redission 加锁示例:
// 如果 lock_key 不存在
if (redis.call('exists', KEYS[1]) == 0)
then
// 设置 lock_key 线程标识 1 进行加锁
redis.call('hset', KEYS[1], ARGV[2], 1);
// 设置过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
// 如果 lock_key 存在且线程标识是当前欲加锁的线程标识
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1)
// 自增
then redis.call('hincrby', KEYS[1], ARGV[2], 1);
// 重置过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
// 如果加锁失败,返回锁剩余时间
return redis.call('pttl', KEYS[1]);
如果客户端需要等待锁释放的话,可以通过
Redis主从部署模式下,锁在主节点加锁成功,命令还未同步到从节点,这个时候主节点挂了,导致数据不一致问题,从而导致并发执行。
当出现脑裂问题的时候,有两个master节点,不同的客户端连接不同的 master 节点时,两个客户端可以同时拥有同一把锁。
lua脚本保证这段复杂业务逻辑执行的原子性。
每次释放锁,次数-1。如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用del命令,从redis里删除这个key。
加锁的时候,传入标识这个客户端的ID,通常用UUID来表示
客户端一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端还持有锁key,那么就会不断的延长锁key的生存时间。