说到Redis分布式锁大部分人都会想到:setnx+lua,或者知道set key value px milliseconds nx。这种实现方式有3大要点(也是面试概率非常高的地方):
事实上这类琐最大的缺点就是它加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况:
正因为如此,Redis作者antirez基于分布式环境下提出了一种更高级的分布式锁的实现方式:Redlock。
在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。我们确保将在N个实例上使用与在Redis单实例下相同方法获取和释放锁。现在我们假设有5个Redis master节点,同时我们需要在5台服务器上面运行这些Redis实例,这样保证他们不会同时都宕掉。
为了取到锁,客户端应该执行以下操作:
Redisson 是用于在 Java 程序中操作 Redis 的库,它使得我们可以在程序中轻松地使用 Redis。Redisson 在 java.util 中常用接口的基础上,为我们提供了一系列具有分布式特性的工具类。下文主要对其分布式锁进行介绍,其他特性暂时未涉及。
redisson已经有对redlock算法封装,接下来对其用法进行简单介绍
添加POM依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.3.2</version>
</dependency>
首先,我们来看一下redission封装的redlock算法实现的分布式锁用法,非常简单,跟重入锁(ReentrantLock)有点类似:
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " + //如果锁名称不存在
"redis.call('hset', KEYS[1], ARGV[2], 1); " +//则向redis中添加一个key为test_lock的set,并且向set中添加一个field为线程id,值=1的键值对,表示此线程的重入次数为1
"redis.call('pexpire', KEYS[1], ARGV[1]); " +//设置set的过期时间,防止当前服务器出问题后导致死锁,return nil; end;返回nil 结束
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +//如果锁是存在的,检测是否是当前线程持有锁,如果是当前线程持有锁
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +//则将该线程重入的次数++
"redis.call('pexpire', KEYS[1], ARGV[1]); " +//并且重新设置该锁的有效时间
"return nil; " + //返回nil,结束
"end; " +
"return redis.call('pttl', KEYS[1]);", //锁存在, 但不是当前线程加的锁,则返回锁的过期时间
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
@Override
public void unlock() {
Boolean opStatus = commandExecutor.evalWrite(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('exists', KEYS[1]) == 0) then " +//如果锁已经不存在(可能是因为过期导致不存在,也可能是因为已经解锁)
"redis.call('publish', KEYS[2], ARGV[1]); " +//则发布锁解除的消息
"return 1; " + //返回1结束
"end;" +
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " + //如果锁存在,但是若果当前线程不是加锁的线
"return nil;" + //则直接返回nil 结束
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " + //如果是锁是当前线程所添加,定义变量counter,表示当前线程的重入次数-1,即直接将重入次数-1
"if (counter > 0) then " + //如果重入次数大于0,表示该线程还有其他任务需要执行
"redis.call('pexpire', KEYS[1], ARGV[2]); " + //则重新设置该锁的有效时间
"return 0; " + //返回0结束
"else " +
"redis.call('del', KEYS[1]); " + //否则表示该线程执行结束,删除该锁
"redis.call('publish', KEYS[2], ARGV[1]); " + //并且发布该锁解除的消息
"return 1; "+ //返回1结束
"end; " +
"return nil;", //其他情况返回nil并结束
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(Thread.currentThread().getId()));
if (opStatus == null) {
throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+ id + " thread-id: " + Thread.currentThread().getId());
}
if (opStatus) {
cancelExpirationRenewal();
}
}
RedLock 这么牛逼算法,还是不完善的,使用 DDIA 作者提出的问题分析下:
案例1
案例2
保险起见,必须由数据库进行兜底。