入坑1(过期时间):
锁没有设置过期时间,程序报错,该锁没有删除,导致锁一致存在,其他线程永远无法获取锁
错误方式
def redis_lock
lock_key = 'redis:nx:key'
lock_val = UUID
-- 设置锁
redis.setnx(lock_key, lock_val)
-- ##########
-- 此时这里报错,,,该锁无法删除!!!
-- ##########
redis.del(lock_key)
end
解决
def redis_lock
lock_key = 'redis:nx:key'
lock_val = UUID
-- 设置锁
redis.setnx(lock_key, lock_val)
-- 设置过期时间
redis.expire(lock_key, 30)
-- ##########
-- 此时这里报错,,,但是设置过期时间,30s后会自动删除锁
-- ##########
redis.del(lock_key)
end
入坑2(加锁原子性):
设置锁的操作不是原子性,导致锁没有设置上上过期时间
伪代码如下:
错误方式
def redis_lock
lock_key = 'redis:nx:key'
lock_val = UUID
-- 设置锁
redis.setnx(lock_key, lock_val)
-- ##########
-- 此时可能因为各种问题没有执行到这里,,,没有设置上过期时间,,,也没有执行到删除key操作,,,这个锁其他线程无法删除!!!
-- ##########
redis.expire(lock_key, 30)
redis.del(lock_key)
end
解决
-- 使用官方推荐的lua脚本,将加锁操作原子化,就不会导致如上的情况出现
def redis_lock
lock_key = 'redis:nx:key'
lock_val = UUID
redis.eval("if redis.call('set', KEY[1], VAL[1], 'EX', '30', 'NX') then return 1; else return 0; end;", [lock_key], [lock_val])
redis.del(lock_key)
end
入坑3(删除其他线程锁):
线程A拿到锁,开始去获取数据,但是这时候,可能由于网络原因导致业务代码长时间没有执行完,锁时间过期,此时线程B来了获取到锁,此时线程A业务执行完成,,,啪一下把线程B的锁直接删了,,,线程B来了锁已经被删了删锁失败。
参考:redis官网lua删除锁
解决方法1(lua脚本)
def redis_lock
lock_key = 'redis:nx:key'
lock_val = UUID
redis.eval("if redis.call('set', KEY[1], VAL[1], 'EX', '30', 'NX') then return 1; else return 0; end;", [lock_key], [lock_val])
-- ##########
-- 删除锁
-- ##########
del_lua = "
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end
"
suc_or_err = redis.eval(del_lua, [lock_key], [lock_val])
if suc_or_err == 1
p '删锁成功'
elsif suc_or_err == 0
p '删锁失败'
end
end
解决方法2(redis事务)
这种侵入性有点大,感觉不太好,还是推荐lua脚本,保证删锁的原子性!
def redis_lock
lock_key = 'redis:nx:key'
lock_val = UUID
redis.eval("if redis.call('set', KEY[1], VAL[1], 'EX', '30', 'NX') then return 1; else return 0; end;", [lock_key], [lock_val])
-- ##########
-- 删除锁
-- ##########
if (redis.get(lock_key) == lock_val)
redis.watch(lock_key)
redis.multi()
redis.del(lock_key)
reids.exec()
end
end
入坑4(reids集群):
如上设置,对于大部分分布式锁产生的问题都可以避免,但是还是会遇到如下的情况:
redis集群问题:
- 客户端A从master获取到锁
- 在master将锁同步到slave之前,master宕掉了。
- slave节点被晋级为master节点
- 客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。安全失效!
如上的情况我们称之为
脑裂
解决方案:RedLock
假设有5个redis节点,这些节点之间既没有主从,也没有集群关系。客户端用相同的key和随机值在5个节点上请求锁,请求锁的超时时间应小于锁自动释放时间。当在3个(超过半数)redis上请求到锁的时候,才算是真正获取到了锁。如果没有获取到锁,则把部分已锁的redis释放掉。
redis锁自动续期:
- 线程A业务代码没有执行完成,后台会启动定时任务循环遍历这些锁,锁过期时间到了业务没有执行完成,自动续期。
此时有需要引入更规范,更规范的解决方式,这里官方已经给了很好的解决方式,redlock,参考官方简介:Redis分布式锁
Redis官方分布式锁框架:RedLock
如上针对不同语言都有对应的解决方案,这里以java的redisson为例讲解:
RedisConfig.java
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
/**
* RedisConfig配置文件中添加redisson加载
*/
@Bean
public Redisson redisson(){
Config config = new Config();
config.useSingleServer().setAddress( "redis://127.0.0.1:3306" ).setDatabase( 0 );
return (Redisson) Redisson.create( config );
}
}
RedisTest.java
@Autowired
private Redisson redisson;
private String RED_LOCK = "test:nx:lock";
@Test
public void testRedisson(){
// 加锁
RLock lock = redisson.getLock( RED_LOCK );
lock.lock();
try{
// 业务代码
} catch (Exception e){
} finally {
// 解锁 [lock.isLocked():锁定状态] [lock.isHeldByCurrentThread():当前线程持有锁]
if (lock.isLocked() && lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}