Redis 分布式锁
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁
特点:
- 多线程可见
- 互斥
- 高可用
- 高性能(高并发)
- 安全性、可重入性、重试机制、锁超时自动续期等 …
加锁之后,对整个分布式集群都有效
- 基于数据库
- redis缓存:使用setnx上锁,使用del释放锁;设置过期时间,自动释放 set user 10 nx ex 120
- zookeeper
实现基于分布式锁需要实现两个方法:
-
获取锁
确保只能有一个线程获取锁,确保添加锁和添加过期时间的原子性
非阻塞:尝试一次,成功返回 true,失败返回 false
set key name ex 10 nx #ex是设置超时时间,nx是互斥
-
释放锁
手动释放
超时释放:获取锁时添加一个超时时间
del key
Redis 分布式锁的初级版本
Lock 接口
public interface ILock {
/**
* 尝试获取锁
*
* @param timeoutSec 锁持有的超时时间,过期自动释放
* @return true:成功/false:失败
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unlock();
}
Lock 实现:
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock:";
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
//获取线程标识
long threadId = Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
//释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
SimpleRedisLock lock = new SimpleRedisLock();
try {
if(lock.tryLock(time)){
//执行业务逻辑
}
} finally {
lock.unlock();
}
存在的问题:
- 业务执行时间过长,导致锁超时,自动释放
- 线程一,锁超时后,线程二又获取到锁,线程一执行完逻辑后,释放锁,此时释放的是线程二的锁
改进 Redis 分布式锁
- 在获取锁时存入线程的标识(可以使用UUID)
- 在释放锁时先获取锁中的标识,判断是否与当前的线程标识是否相等,是,则释放;不是,则不释放
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
//获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
//获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
//获取锁标识
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
if (threadId.equals(id)) {
//释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
}
存在的问题:
unlock 中判断锁标识的操作和释放锁的操作不是原子操作,如果threadId.equals(id)
判断成功之后,产生了阻塞(如:Full GC时),导致多线程安全问题,解决方法可以使用 Lua 脚本,保证以上操作的原子性
再次改进 Redis 分布式锁
Redis提供了 Lua 脚本功能,在脚本中编写多条命令,确保多条命令执行时的原子性
释放锁思路:
- 获取锁中的线程标识
- 判断是否与当前的标识一致
- 如果一致则释放(删除)锁,否则什么都不做
-- 锁的key
local key = KEY[1]
-- 当前线程标识
local threadId = ARGV[1]
-- 获取锁中的标识
local id = redis.call('get', key)
-- 比较线程标识与锁标识是否一致
if(id == threadId) then
-- 释放锁
return redis.call('del', key)
end
return 0
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
//初始化脚本
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
//加载 Lua 脚本
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
//获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
//调用释放锁的 Lua 脚本
stringRedisTemplate.execute(UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
}
Redisson
基于上述setnx
实现的分布式锁还存在以下问题
- 不可重入:同一个线程无法获取同一把锁
- 不可重试:获取锁只尝试一次就返回 false,没有重试机制
- 超时释放:锁超时释放虽然可以避免死锁,但是如果业务执行时间过长,也会导致锁释放,存在安全隐患
- 主从一致性:如果 Redis 提供了主从集群,主从同步存在延迟,当主机宕机时,尚未同步至从节点,会出现安全问题
Redisson是一个在 Redis 基础实现分布式工具的集合,包括分布式锁