分布式系统在多线程开发中,经常遇到需要对资源加锁的情况,redis分布式锁是比较常见的一种解决方案。下面对Redis集群中使用分布式锁所遇到的问题给出一些解决方案
问题1
如何避免死锁?
当用户1拿到锁以后,若进程挂了、或因为别的原因,没有机会主动释放锁,会导致已经获得锁的客户端一直占用锁,其他客户端永远无法获取到锁。
解决方案:
为了解决以上死锁问题,最容易想到的方案是:在申请锁时,在Redis中实现时,给锁设置一个过期时间,假设操作共享资源的时间不会超过10s,那么加锁时,给这个key设置10s过期即可。并且Redis 2.6.12之后,Redis扩展了SET命令的参数,可以在SET的同时指定EXPIRE时间,这条操作是原子的,例如以下命令是设置锁的过期时间为10秒。
问题2
Redis分布式锁误删
- 持有锁的线程1在锁的内部出现了阻塞,而他的锁超时自动释放(del了),这时其他线程,线程2来尝试获得锁,就拿到了这把锁(setnx了);
- 2.然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除(把线程2的setnx的值del了)。
解决方案
一:设置redis分布式锁超时时间t时,使t 大于该锁所在线程的超时时间(线程超时时间一般就是该线程调用的接口超时时间)
二:在每一次释放锁之前,判断当前的锁是否属于自己这个线程,这样就避免了释放别人锁的情况。
核心逻辑:在存入锁时,放入自己线程的标识(可以将线程ID拼接到redis key里),在删除锁时,判断当前这把锁的标识是不是自己存入的,如果是,则进行删除,如果不是,则不进行删除。
问题三
Redis分布式锁原子性问题
由于redis 分布式锁key中的线程id是否属于本线程,删除该key,这两个步骤并不是原子性的:假如在delete key的时候发生了阻塞,而导致超时释放锁,将造成以下后果:
此时线程2获取到了锁,正在嘎嘎执行业务的时候,线程1的del阻塞结束了,但由于在判断句内部,这个锁仍然会被释放(即线程2的锁仍然被认为是线程1的,被释放了),这时候线程3进来,又会发生一样的安全问题。
解决方案:
Lua脚本解决多命令原子性问题
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。 lua脚本如下:
– 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
– 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call(‘GET’, KEYS[1]) == ARGV[1]) then
– 一致,则删除锁
return redis.call(‘DEL’, KEYS[1])
end
– 不一致,则直接返回
对应的java 代码
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
public void unlock() {
// 调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}