本文主要介绍 Redis 分布式锁误删问题的解决
场景一
1. 问题的产生情况一
因为业务阻塞,导致别人的锁被误删
2. 解决思路
获取锁的时候存入标识,释放锁的时候判断标识是否一致,一致可以释放锁,不一致不释放锁。
3. 解决代码
总体思路:
- 获取锁的时候存入线程标识 , 用 UUID 表示 ()
- 释放锁的时候,判断标识是否一致
public class SimpleRedisLock implements ILock{
// 锁的 key 前缀
private static final String KEY_PREFIX = "lock:";
// 线程 id 的前缀
private static final String ID_PREFIX = UUID.randomUUID().toString() + "-";
// 锁的名字
private String lockName;
// 传入的 StringRedisTemplate
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String lockName, StringRedisTemplate stringRedisTemplate) {
this.lockName = lockName;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
// 获取当前线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+lockName, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success); // 防止自动拆箱时候出现空指针问题
}
@Override
public void unlock() {
// 获取当前锁的线程 id
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + lockName);
// 获取当前请求释放锁的线程 id
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 判断两者是否一致,一致给释放锁
if(threadId.equals(id)){
stringRedisTemplate.delete(KEY_PREFIX + lockName);
}
}
}
场景二
1. 问题的产生情况二
获取锁标识并判断一致后被阻塞导致误删问题
2. 解决思路
保证判断锁标识一致和删除锁这一操作的原子性
- Redis 事务:
- 支持原子性,不支持一致性
- 批处理操作,最终一致性 (基于乐观锁)
- Lua 脚本
Redis 提供了 Lua 脚本功能,在一个脚本中编写多条 Reids 命令,保证多条命令执行的原子性
https://www.runoob.com/lua/lua-tutorial.html
Redis 调用函数:
redis.call('命令名称', 'key', '其他参数')
Redis 执行脚本的命令:
eval "脚本语句"
eval 支持带参数脚本:
eval "return redis.call('set', KEYS[1], ARGV[1])" 1 name Rose
等价于 set name Rose
3. 解决代码
SpringBoot 整合 Lua 脚本实现锁的释放
释放锁的 Lua 脚本:
-- 获取锁中的线程标识
local id = redis.call('get, KEYS[1])
-- Redis中存入的线程标识和传入的参数一致可以删除
if(id == ARGV[1]) then
return redis.call('del', KEYS[1])
end
return 0
Java 执行 Lua 脚本:
public class SimpleRedisLock implements ILock{
// 锁的 key 前缀
private static final String KEY_PREFIX = "lock:";
// 线程 id 的前缀
private static final String ID_PREFIX = UUID.randomUUID().toString() + "-";
// 锁的名字
private String lockName;
// 传入的 StringRedisTemplate
private StringRedisTemplate stringRedisTemplate;
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 SimpleRedisLock(String lockName, StringRedisTemplate stringRedisTemplate) {
this.lockName = lockName;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
// 获取当前线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+lockName, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success); // 防止自动拆箱时候出现空指针问题
}
@Override
public void unlock() {
stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX + lockName), ID_PREFIX + Thread.currentThread().getId());
}
}
总结:
Redis 分布式锁实现思路:
- 利用 setnx 获取锁,设置过期时间,保存 value 为线程标识
- 释放锁时先判断线程标识是否一致,一致则删除锁
特性:
- setnx 保证互斥性
- 通过设置过期时间的方式,保证出现故障时锁仍然能释放,避免了死锁
- 利用 Redis 集群保证高可用性和并发现