分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁
分布式锁的核心是实现多进程之间锁的互斥,而满足这一点的方式有很多,常见的有三种:
锁获取了,还没有来得及设置过期时间服务器就宕机了
保证 setnx(获取锁)和 expire 设置过期时间两个操作是原子性的
需求:定义一个类,实现下面的接口,利用 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,避免线程 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);
}
}
}
解决方案:Lua 脚本
Redis 提供了 Lua 脚本功能,在一个脚本中编写多条 Redis 命令,确保多条命令执行时的原子性
Lua 是一种编程语言 https://www.runoob.com/lua/lua-tutorial.html
参数有两种:key 类型参数,其他参数
如果脚本中的 key(gender)、value(handsomeBoy)不想写死,可以作为参数传递
key 类型参数会放入 KEYS 数组
其它参数会放入 ARGV 数组,在脚本中可以从 KEYS 和 ARGV 数组获取这些参数
Lua 语言中下标从 1 开始
获取锁(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
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());
}
}