Redis分布式锁其实是使用Redis实现的分布式锁,利用了Redis的key过期机制,SETNX机制(即如果不存在对应的key才可以set成功),但如何去实现这样一把分布式锁主要是Redis客户端框架来做的事,事实上锁这个高层API就是由这样的客户端框架暴露出来的,比如jedis,redisson等。接下来我们就分别看看jedis,redisson这样的框架底层如何与redis打交道,然后对上如何暴露简单的锁API。
Jedis分布式锁机制
引入jar包:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
然后来一个使用锁的demo:
public class RedisTest {
private static final String LOCK_SUCCESS = "OK";
private static final Long RELEASE_SUCCESS = 1L;
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final int expireTime = 30000;
private Jedis jedis = new Jedis();
public void testLock(){
//加锁
boolean mylock = this.tryLock("mylock");
try {
//如果加锁成功
if(mylock){
//具体的业务代码
}
} finally {
// 4.解锁
this.releaseLock("mylock");
}
}
public boolean tryLock(String lockKey) {
String threadId = String.valueOf(Thread.currentThread().getId());
String result = jedis.set(lockKey, threadId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
public boolean releaseLock(String lockKey) {
String threadId = String.valueOf(Thread.currentThread().getId());
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(threadId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
以上例子中,至于获取锁失败后的重试暂不考虑。
加锁:
可以从上面的demo中看出jedis加锁使用的程序语句是:
jedis.set(lockKey, threadId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime)
简单追一下源码便可以知道,其实是把命令:
SET key value NX PX expireTime
发送给了redis,redis判断如果这个key不存在就设置成功,并且设置key的过期时间 expireTime;如果redis判断这个key存在就设置失败。
解锁:
解锁使用的是lua脚本来实现的:
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
因为要先判断一下是不是加锁的那个人,当判断是加锁的那个人的时候才可以进行解锁。(其中value记录了一个加锁人才知道的值,这里我们简单处理,记录成了当前线程ID值)。
因为有这样的先判断再执行两个步骤,要保证原子性就必要用使用lua脚本,然后lua脚本会作为一个命令发送给redis,redis在执行lua脚本时会保证原子性,即把它当做一个最小粒子的命令来执行。相反,如果不使用lua脚本,那么必然需要两条命令,两条命令会分别发给redis,分别执行,网络通讯效率低,而且有原子性风险。
总结:
jedis对于分布式锁的支持不太好,并没有封装好分布式锁API对外暴露,只是简单的封装了一下redis命令,如果要项目中依赖了jedis又要使用分布式锁,就只能自己去封装实现分布式锁了,方方面面都需要考虑到。
Redisson分布式锁机制
引入jar包:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.7</version>
</dependency>
先来一个使用demo看看:
public class RedissonLockTest {
public void testLock(){
// 1. Create config object
Config config = new Config();
config.useClusterServers()
// use "rediss://" for SSL connection
.addNodeAddress("redis://127.0.0.1:7181")
.addNodeAddress("redis://127.0.0.1:7182")
.addNodeAddress("redis://127.0.0.1:7183");
// 2. Create Redisson instance
// Sync and Async API
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("myLock");
// 3.加锁
lock.lock();
try {
//具体业务代码
} finally {
// 4.解锁
lock.unlock();
}
}
}
封装的很好,getLock之后,加锁解锁API和JDK的ReentrantLock一样简洁!接下来我们追源码看看Redisson底层是如何与Redis打交道实现加锁、、锁等待 以及 解锁 的。
获取锁:
redisson.getLock("myLock")追下去,发现其实获取的是一个RedissonLock:
@Override
public RLock getLock(String name) {
//这里传入的name就是锁的名称,用于标识一把锁,后面会用到
return new RedissonLock(commandExecutor, name);
}
lock.lock()其实调用的RedissonLock.lock():
@Override
public void lock() {
try {
//第一个参数为leaseTime,默认传-1
lock(-1, null, false);
} catch (InterruptedException e) {
throw new IllegalStateException();
}
}
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
//第一个参数为waitTime,默认传-1
//使用当成线程ID,肯定是用于标识当前“操作人”,但不同实例中的线程ID可能是会重复的哦,看看后面会怎么来处理
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return;
}
//省略其他不相关代码...
}
这里只有tryAcquire()方法是我们需要关注的,继续追下去:
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture<Long> ttlRemainingFuture;
if (leaseTime != -1) {
ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
//省略不相关代码...
}
从前面可以发现传入的leaseTime默认为-1,所以进else分支语句里面的tryLockInnerAsync():
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
//要发给Redis的lua脚本以及参数
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"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; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
原来就是给Redis发了一段lua脚本嘛,咦,脚本里面的KEYS[1], ARGV[1], ARGV[2] 是什么东东?点进去看看evalWriteAsync()方法的定义:
protected <T> RFuture<T> evalWriteAsync(String key, Codec codec, RedisCommand<T> evalCommandType, String script, List<Object> keys, Object... params) {
//省略不相关代码...
}
keys就是我们传入的Collections.singletonList(getRawName())嘛,对应的就是KEYS[1],getRawName()获取到的就是前面传入的锁名称,结合lua脚本可以知道:把锁名称作为了hash结构外层那个key!
params就是unit.toMillis(leaseTime), getLockName(threadId),而 getLockName(threadId)里面就是返回了 当前连接Redis的ConnectionManager的id 加上 当前线程ID,对应lua脚本里面的ARGV[2],也就是hash结构里面那个key!这样才可以唯一标识一个“操作人”嘛。
unit.toMillis(leaseTime)就对应了乱脚本里面的ARGV[1],作为key的过期时间。
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"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; " +
"end; " +
"return redis.call('pttl', KEYS[1]);"
为了便于表述,先提前说明一下:
- hash key: hash结构外面的key
- hash inner key: hash结构里面那个key
这段lua脚本的意思就是:先判断一个指定的锁名称的hash key是否存在,如果不存在的话,给指定的hash inner key进行自增,并给hash key一个过期时间,返回;如果hash key存在的话,看看hash inner key是否存在,如果存在的话,如果存在的话说明是当前持有锁的人来了,进行自增,并刷新hash key的过期时间,返回;如果hash inner key不存在的话,说明不是当前持有锁的人,获取一下这个 hash key的的存活时间,返回。
维持锁:
那锁有过期时间,如果在持有锁期间,锁过期了怎么办呢?redisson开发者肯定想得到,所以有了对应的watch dog机制,具体源码就在tryAcquireAsync中的scheduleExpirationRenewal():
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture<Long> ttlRemainingFuture;
if (leaseTime != -1) {
ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
if (ttlRemaining == null) {
if (leaseTime != -1) {
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
//过期时间刷新机制
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
追下去会发现这样一个核心方法:
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getRawName()),
internalLockLeaseTime, getLockName(threadId));
}
还是lua脚本,先判断了一下自己是否是锁的持有人,如果是的话,那么就刷新一下hash key的存活时间。
锁等待:
从上面的获取锁的那段lua脚本分析中我们可以知道:对于获取锁失败的线程,会获取一下hash key的存活时间然后返回。返回到了这里:
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return;
}
RFuture<RedissonLockEntry> future = subscribe(threadId);
if (interruptibly) {
commandExecutor.syncSubscriptionInterrupted(future);
} else {
commandExecutor.syncSubscription(future);
}
try {
while (true) {
ttl = tryAcquire(-1, leaseTime, unit, threadId);
// 如果ttl 为null,说明是获取锁的线程,直接跳出循环即可
if (ttl == null) {
break;
}
// 如果ttl不为空,说明是获取锁失败的线程
if (ttl >= 0) {
//如果ttl大于0,说明锁还没有过期,当前线程会等待指定时间之后再去尝试获取锁
try {
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
//如果ttl小于0,那么说明锁已过期,当前线程会立刻尝试去获取锁
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
} finally {
unsubscribe(future, threadId);
}
// get(lockAsync(leaseTime, unit));
}
然后在while循环中,不断重新尝试获取锁,直到成功。当然轮询的时候会根据拿到的ttl,等待指定时间之后,再尝试去获取锁。
释放锁:
@Override
public void unlock() {
try {
get(unlockAsync(Thread.currentThread().getId()));
} catch (RedisException e) {
if (e.getCause() instanceof IllegalMonitorStateException) {
throw (IllegalMonitorStateException) e.getCause();
} else {
throw e;
}
}
}
@Override
public RFuture<Void> unlockAsync(long threadId) {
RPromise<Void> result = new RedissonPromise<>();
RFuture<Boolean> future = unlockInnerAsync(threadId);
//省略不相关代码...
}
查看最普通的锁的释放过程:RedissonLock.unlockAsync()
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
"return nil;",
Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
lua脚本解释:如果不是当前持有锁的人,那么立刻返回;如果是,那么对hash inner key 进行减一操作,然后查看结果值,如果结果值大于0,那么刷新hash key的过期时间;如果结果值小于等于0,那么就删除hash key,并发布锁释放的消息。
总结:
redisson才真正去封装出了分布式锁,底层通过lua脚本与redis打交道,上层暴露出极其简单的类似ReentranLock的锁API。
分布式锁就是利用这样子的一个结构:
lockName: {
connectionManagerId+currentThreadId : 1
}
lockName就是自定义的锁名称,connectionManagerId就是当前连接的ID,currentThreadId是当前线程ID,1是当前锁没有重入过,如果重入一次,会变为2
分布式锁的整体实现逻辑可以概括为:
获取锁:先判断一个指定的锁名称的 hash key是否存在,如果不存在的话,给指定的hash inner key进行自增,并给hash key一个过期时间,获取锁成功,返回;如果存在的话,看看hash inner key是否存在,如果存在的话,如果存在的话说明是当前持有锁的人来了,进行锁重入:对hash inner key进行自增,并刷新hash key的过期时间,返回;如果hash inner key不存在的话,说明不是当前持有锁的人,获取锁失败,获取一下这个 hash key的的存活时间,返回。
维持锁:先判断了一下自己是否是锁的持有人,如果是的话,那么就刷新一下hash key的存活时间。
等待锁:获取锁失败之后,在while循环中,不断重新尝试获取锁,直到成功。当然轮询的时候会根据拿到的ttl,等待指定时间之后再轮询。
释放锁:如果不是当前持有锁的人,那么立刻返回;如果是,那么对hash inner key 进行减一操作,然后查看结果值,如果结果值大于0,那么刷新hash key的过期时间;如果结果值小于等于0,那么就删除hash key,并发布锁释放的消息。