目录
- 一、分布式锁实现原理
- 二、不同的分布式锁实现方案
- 三、Redis 的 setnx 实现互斥锁
- 四、基于 Redis 实现分布式锁初级版
- 五、误删锁问题(业务阻塞导致)
- 六、误删锁(Redis 命令原子性导致)
- (1) Lua 脚本
- (2) Redis 编写和执行 Lua 脚本
- (3) 复杂逻辑的 Lua 脚本(业务相关)
- (4) RedisTemplate 执行 Lua 脚本
一、分布式锁实现原理
🎄 分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁
二、不同的分布式锁实现方案
🎄 分布式锁的核心是实现多进程之间锁的互斥,而满足这一点的方式有很多,常见的有三种:
三、Redis 的 setnx 实现互斥锁
🎄锁获取了,还没有来得及设置过期时间服务器就宕机了
🎄保证 setnx(获取锁)和 expire 设置过期时间两个操作是原子性的
四、基于 Redis 实现分布式锁初级版
🎄 需求:定义一个类,实现下面的接口,利用 Redis 实现分布式锁功能
public interface LockInter {
/**
* 尝试获取锁
*
* @param ttlSecond 锁的过期时间
* @return true: 成功获取锁; false: 获取锁失败
*/
boolean tryLock(long ttlSecond);
/**
* 释放锁
*/
void unlock();
}
public class LockImplV1 implements LockInter {
private String name; // 和业务相关的锁的名字
private StringRedisTemplate stringRedisTemplate;
private static final String LOCK_KEY_PREFIX = "lock:";
public LockImplV1(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long ttlSecond) {
String key = LOCK_KEY_PREFIX + name;
// value 里面放当前线程的唯一标识(线程 ID)
String val = Thread.currentThread().getId() + "";
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key,
val,
ttlSecond,
TimeUnit.SECONDS);
// Boolean -- boolean 会自动拆箱
// 当 success 为 null 的时候会抛异常
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
stringRedisTemplate.delete(LOCK_KEY_PREFIX + name);
}
}
五、误删锁问题(业务阻塞导致)
需求:修改之前的分布式锁实现,满足:
- 在获取锁时存入线程标识(可以用 UUID 表示)
- 在释放锁时先获取锁中的线程标识,判断是否与当前线程标识一致
① 如果一致则释放锁
② 如果不一致则不释放锁
要用 UUID,避免线程 ID 重复
public class LockImplV2 implements LockInter {
private String name; // 和业务相关的锁的名字
private StringRedisTemplate stringRedisTemplate;
private static final String LOCK_KEY_PREFIX = "lock:";
private static final String UNIQUE_PREFIX = UUID.randomUUID().toString(true);
public LockImplV2(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long ttlSecond) {
String key = LOCK_KEY_PREFIX + name;
// value 里面放当前线程的唯一标识(线程 ID)
String val = UNIQUE_PREFIX + Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key,
val,
ttlSecond,
TimeUnit.SECONDS);
// Boolean -- boolean 会自动拆箱
// 当 success 为 null 的时候会抛异常
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
String k = LOCK_KEY_PREFIX + name;
String cacheVal = stringRedisTemplate.opsForValue().get(k);
String curVal = UNIQUE_PREFIX + Thread.currentThread().getId();
if (curVal.equals(cacheVal)) {
stringRedisTemplate.delete(k);
}
}
}
六、误删锁(Redis 命令原子性导致)
解决方案:Lua 脚本
(1) Lua 脚本
📖 Redis 提供了 Lua 脚本功能,在一个脚本中编写多条 Redis 命令,确保多条命令执行时的原子性
📖 Lua 是一种编程语言 https://www.runoob.com/lua/lua-tutorial.html
(2) Redis 编写和执行 Lua 脚本
参数有两种:key 类型参数,其他参数
📖 如果脚本中的 key(gender)、value(handsomeBoy)不想写死,可以作为参数传递
📖 key 类型参数会放入 KEYS 数组
📖 其它参数会放入 ARGV 数组,在脚本中可以从 KEYS 和 ARGV 数组获取这些参数
Lua 语言中下标从 1 开始
(3) 复杂逻辑的 Lua 脚本(业务相关)
📖 获取锁(Redis 缓存)中的线程标识 cacheVal
📖 判断是否与当前线程标识一致 curVal
📖 如果一致则释放锁(del)
📖 如果不一致则什么都不做
上述操作要通过 Lua 脚本执行,保证多条 Redis 命令的原子性(防止误删锁)
--- 当前线程的线程标识
local curVal = ARGV[1]
--- 要删除的锁的 key
local lockKey = KEYS[1]
if(cacheVal == curVal)
then
return redis.call('DEL', KEYS[1])
end
return 0
(4) RedisTemplate 执行 Lua 脚本
Lua 脚本可写在 Java 的类路径下的资源文件夹中
public class LockImplV3 implements LockInter {
private String name; // 和业务相关的锁的名字
private StringRedisTemplate stringRedisTemplate;
private static final String LOCK_KEY_PREFIX = "lock:";
private static final String UNIQUE_PREFIX = UUID.randomUUID().toString(true);
private static final DefaultRedisScript<Long> UNLOCK_LUA_SCRIPT;
static { // 初始化 UNLOCK_LUA_SCRIPT
UNLOCK_LUA_SCRIPT = new DefaultRedisScript<>();
UNLOCK_LUA_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_LUA_SCRIPT.setResultType(Long.class);
}
public LockImplV3(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long ttlSecond) {
String key = LOCK_KEY_PREFIX + name;
// value 里面放当前线程的唯一标识(线程 ID)
String val = UNIQUE_PREFIX + Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key,
val,
ttlSecond,
TimeUnit.SECONDS);
// Boolean -- boolean 会自动拆箱
// 当 success 为 null 的时候会抛异常
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
stringRedisTemplate.execute(
UNLOCK_LUA_SCRIPT,
Collections.singletonList(LOCK_KEY_PREFIX + name),
UNIQUE_PREFIX + Thread.currentThread().getId());
}
}