一、Redis实现分布式锁原理
Redis
所谓分布式锁,需要满足以下特性:
- 独占性:对同一把锁,在同一时刻只能被同一个客户端占有,因此体现了互斥性。
- 健壮性:即不能产生死锁(dead lock). 占有锁客户端因宕机获取锁失失败或过期立即返回执行解锁动作,锁可以被正常使用,不会造成客户端县城阻塞。
- 对称性:加锁和解锁的使用方必须为同一身份. 不允许非法释放他人持有的分布式锁
- 高可用:当提供分布式锁服务的基础组件中存在少量节点发生故障时,应该不能影响到分布式锁服务的稳定性
二、实现步骤
根据以上条件,可以大致设想出以下的Redis锁实现方案:
- 使用
SETNX
命令尝试设置锁的值,如果返回1表示成功获取锁,否则表示获取锁失败。 - 使用
GET
命令获取锁的值,判断当前客户端是否持有锁,如果持有锁则将锁的值加1,否则返回获取锁失败。 - 使用
DEL
命令释放锁。 - 使用过期时间来防止死锁,锁的过期时间应该大于业务处理的时间,一般为几秒到几分钟。
集成springIntegrationRedis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-redis</artifactId>
</dependency>
配置Redis分布式锁
@Configuration
public class RedisLockConfig {
/**
* 锁过期毫秒数
*/
private static final long EXPIRE_AFTER_MILLS = 600000L;
@Bean(destroyMethod = "destroy")
public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory) {
return new RedisLockRegistry(redisConnectionFactory, "redis-lock", EXPIRE_AFTER_MILLS);
}
}
// 第一种实现方式
@Resource
private RedisLockRegistry redisLockRegistry;
public void RedisLockTest(String key) {
Lock lock = redisLockRegistry.obtain(key);
// 尝试解锁,当前锁是否在使用
if (!lock.tryLock()) {
// 执行其他业务逻辑
return;
}
try {
// 加锁
lock.lock();
//业务逻辑
} finally {
//释放锁
lock.unlock();
}
}
//第二种实现方式
@RedisLock("redis-lock")
public void bussinessMehod(){
}
三、原理分析
obtain方法
public final class RedisLockRegistry implements ExpirableLockRegistry, DisposableBean{
private final Map<String, RedisLock> locks = new ConcurrentHashMap<>();
private final String registryKey;
@Override
public Lock obtain(Object lockKey) {
Assert.isInstanceOf(String.class, lockKey);
String path = (String) lockKey;
// 如果 key 对应的 value 不存在,则使用获取 remappingFunction 重新计算后的值,并保存为该 key 的 value,否则返回 value。
return this.locks.computeIfAbsent(path, RedisLock::new);
}
private final class RedisLock implements Lock {
private final String lockKey;
private RedisLock(String path) {
this.lockKey = constructLockKey(path);
}
private String constructLockKey(String path) {
return RedisLockRegistry.this.registryKey + ":" + path;
}
}
}
根据key查找ConcurrentHashMap类型的locks
集合,其中是否存在key,如存在就返回内部类RedisLock
,不存在以当前registryKey
类变量为key,调用RedisLock构造方法,并放入map集合。
补充
:每个应用都会创建一个RedisLockRegistry实例,且ConcurrentHashMap是一个支持高并发更新与查询的map集合,因此不同线程之间会共享RedisLock类。
tryLock方法
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {
private @Nullable ScriptExecutor<K> scriptExecutor;
@Override
public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {
return scriptExecutor.execute(script, keys, args);
}
}
local lockClientId = redis.call('GET', KEYS[1])
if lockClientId == ARGV[1] then
redis.call('PEXPIRE', KEYS[1], ARGV[2])
return true
elseif not lockClientId then
redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])
return true
end
return false
在研究tryLock
方法之前,需解下obtainLock
这私有方法。具体逻辑是调用RedisLockRegistry的内置Redis Lua脚本获取分布式锁key对应的value,结合上面代码和脚本可以看出execute
方法key和args替换脚本KEYS
和ARGV
,并生成最终脚本内容,
- 为保证分布式锁对称性须校验
lockClientId
和客户端一致,如一致则为重入锁,需重设置过期时间expireAfter
(默认60s) 并返回成功; - 分布式锁lockClientI和key不存在,则设置传入RedisLock中私有变量
lockKey
为新key、当前lockClientId
,返回成功; - 以上两项都不满足,当前锁被占用,上锁失败,返回失败;
public final class RedisLockRegistry implements ExpirableLockRegistry, DisposableBean {
private final RedisScript<Boolean> obtainLockScript;
private final StringRedisTemplate redisTemplate;
private static final String OBTAIN_LOCK_SCRIPT =
"local lockClientId = redis.call('GET', KEYS[1])\n" +
"if lockClientId == ARGV[1] then\n" +
" redis.call('PEXPIRE', KEYS[1], ARGV[2])\n" +
" return true\n" +
"elseif not lockClientId then\n" +
" redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])\n" +
" return true\n" +
"end\n" +
"return false";
// 构造函数
public RedisLockRegistry(RedisConnectionFactory connectionFactory, String registryKey, long expireAfter) {
Assert.notNull(connectionFactory, "'connectionFactory' cannot be null");
Assert.notNull(registryKey, "'registryKey' cannot be null");
this.redisTemplate = new StringRedisTemplate(connectionFactory);
this.obtainLockScript = new DefaultRedisScript<>(OBTAIN_LOCK_SCRIPT, Boolean.class);
this.registryKey = registryKey;
this.expireAfter = expireAfter;
}
//内部类 实现了Lock接口
private final class RedisLock implements Lock {
private final ReentrantLock localLock = new ReentrantLock();
@Override
public boolean tryLock() {
try {
return tryLock(0, TimeUnit.MILLISECONDS);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
long now = System.currentTimeMillis();
if (!this.localLock.tryLock(time, unit)) {
return false;
}
try {
long expire = now + TimeUnit.MILLISECONDS.convert(time, unit);
boolean acquired;
while (!(acquired = obtainLock()) && System.currentTimeMillis() < expire) { //NOSONAR
Thread.sleep(100); //NOSONAR
}
if (!acquired) {
this.localLock.unlock();
}
return acquired;
}
catch (Exception e) {
this.localLock.unlock();
rethrowAsLockException(e);
}
return false;
}
private boolean obtainLock() {
Boolean success = RedisLockRegistry.this.redisTemplate.execute(RedisLockRegistry.this.obtainLockScript, Collections.singletonList(this.lockKey), RedisLockRegistry.this.clientId, String.valueOf(RedisLockRegistry.this.expireAfter));
boolean result = Boolean.TRUE.equals(success);
if (result) {
this.lockedAt = System.currentTimeMillis();
}
return result;
}
}
}
通过ReentrantLock
获取本获取本地锁,不存在直接返回,当前锁若存在但obtainLock()
返回失败,且未超过过期时间则锁转为自旋锁
睡眠100ms后进行下次重试,直到拿到redis锁循环结束,返回成功,若超时了还没拿到,释放锁,返回失败
补充:
ReentrantLock是一个可重入的互斥锁
unlock方法
private final class RedisLock implements Lock {
@Override
public void unlock() {
if (!this.localLock.isHeldByCurrentThread()) {
throw new IllegalStateException("You do not own lock at " + this.lockKey);
}
if (this.localLock.getHoldCount() > 1) {
this.localLock.unlock();
return;
}
try {
if (!isAcquiredInThisProcess()) {
throw new IllegalStateException("Lock was released in the store due to expiration. " +
"The integrity of data protected by this lock may have been compromised.");
}
// 当前线程是否已销毁
if (Thread.currentThread().isInterrupted()) {
RedisLockRegistry.this.executor.execute(this::removeLockKey);
}
else {
removeLockKey();
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Released lock; " + this);
}
}
catch (Exception e) {
ReflectionUtils.rethrowRuntimeException(e);
}
finally {
this.localLock.unlock();
}
}
}
// 当前线程是否持有锁,
public boolean isHeldByCurrentThread() {
return sync.isHeldExclusively();
}
// 当前线程持有该锁次数
public int getHoldCount() {
return sync.getHoldCount();
}
// 当前客户端持有锁跟当前是否一致
public boolean isAcquiredInThisProcess() {
return RedisLockRegistry.this.clientId.equals(RedisLockRegistry.this.redisTemplate.boundValueOps(this.lockKey).get());
}
// 释放当前锁
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
释放锁的过程就表简单,isHeldByCurrentThread()
方法获取当前线程是否持有锁,若不是则抛出当前锁不被持有异常,getHoldCount()
方法获取当前线程持有该锁次数,次数>1,释放当前锁且当前线程持有锁的计数-1
四、总结
RedisLockRegistry通过本地锁ReentrantLock
和Redis
锁的双重锁实现,使用ReentrantLock可重入锁,RedisLockRegistry对锁无续期操作;只适用于单实例的情况下,key过期,还能通过本地锁保证,但多实例下无法通过本地锁保证,会有问题。在分布式系统中,锁的作用非常重要,合理有效的利用锁可以保证系统的安全性和高效性。Redis分布式锁是一种比较常用的方式,通过Redis的高性能和分布式特性,可以方便地实现分布式锁的功能。开发人员可以根据业务需求和系统性能需求来决定是否使用Redis分布式锁,
[1]Java三种方式实现redis分布式锁[EB/OL].
[2]redis 分布式锁进阶篇[EB/OL].
[3]Java中使用Redis实现分布式锁[EB/OL].