学习资料:https://www.jianshu.com/p/f302aa345ca8
今天工作上遇到需要用分布式锁的一个问题,然后当然不是自己写一个分布式锁,公司有中间件可以直接用(封装好的),用起来分分钟,但是不知道其原理,那就不行了,所以根据源码一直走下去和看别人博客,了解了下RedissonRedLock的原理。
查看人家的源码,学习别人的写法是很有必要的。公司的源码就不贴出来了。记录一下RedissonRedLock整个流程吧
1,初始化链接RedissonClient
要使用RedissonRedLock,必须先有redis链接,RedissonClient这个类就是初始化redis链接的。初始化RedissonClient就需要地址密码数据库必要条件啦,通过Config来创建RedissonClient
Set<RedissonClient> clients = new HashSet<>();
for (LockDTO lock : locks) {
Config config = new Config();
SingleServerConfig singleServerConfig = config.useSingleServer();
singleServerConfig.setAddress(lock.getAddress()).setDatabase(lock.getDb());
if (StringUtils.isNotBlank(lock.getPassword())) {
singleServerConfig.setPassword(lock.getPassword());
}
//执行create创建链接,这里链接放在set里,可以创建多个
clients.add(Redisson.create(config));
}
class LockDTO {
String address;
String password;
int db = 0;
}
2,新建RedissonRedLock
因为tryLock和unLock是RedissonRedLock的方法,所以new出RedissonRedLock的对象
//RedissonRedLock构造方法
public RedissonRedLock(RLock... locks) {
super(locks);
}
//根据构造方法来
List<RLock> rLockList = new ArrayList<>();
for (RedissonClient client : clients) {
rLockList.add(client.getLock(key));
}
//
RedissonRedLock redLock = new RedissonRedLock(rLockList.toArray(转数组));
3,尝试获取锁,tryLock其实是RedissonMultiLock的方法
入参数:waitTime:最多等待时间,leaseTime锁过期时间,unit 单位
我的版本是2.15.1,学习博客的是2.14的,源码有不同,基本类似。
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long newLeaseTime = -1;
if (leaseTime != -1) {
newLeaseTime = unit.toMillis(waitTime)*2;
}
long time = System.currentTimeMillis();
long remainTime = -1;
if (waitTime != -1) {
remainTime = unit.toMillis(waitTime);
}
// 等待时间
long lockWaitTime = calcLockWaitTime(remainTime);
//加锁失败时间限制
int failedLocksLimit = failedLocksLimit();
List<RLock> acquiredLocks = new ArrayList<RLock>(locks.size());
//遍历RLock锁
for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
RLock lock = iterator.next();
boolean lockAcquired;
try {
//没有等待时间或者失效时间就直接获取锁
if (waitTime == -1 && leaseTime == -1) {
//最底层的获取锁
lockAcquired = lock.tryLock();
} else {
//根据等待时间和失效时间去获取锁
long awaitTime = Math.min(lockWaitTime, remainTime);
//最底层的获取锁
lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
}
} catch (RedisResponseTimeoutException e) {
//异常了需要解锁
unlockInner(Arrays.asList(lock));
lockAcquired = false;
} catch (Exception e) {
lockAcquired = false;
}
if (lockAcquired) {
//获取成功了就加入集合
acquiredLocks.add(lock);
} else {
//达到了失败限制就退出循环,不获取锁
if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
break;
}
//失效限制为0就需要把锁解除
//底层有用到AtomicInteger
if (failedLocksLimit == 0) {
unlockInner(acquiredLocks);
if (waitTime == -1 && leaseTime == -1) {
return false;
}
failedLocksLimit = failedLocksLimit();
acquiredLocks.clear();
// reset iterator
while (iterator.hasPrevious()) {
iterator.previous();
}
} else {
failedLocksLimit--;
}
}
//等待时间判断,超过等待时间就解锁
if (remainTime != -1) {
remainTime -= (System.currentTimeMillis() - time);
time = System.currentTimeMillis();
if (remainTime <= 0) {
unlockInner(acquiredLocks);
return false;
}
}
}
if (leaseTime != -1) {
List<RFuture<Boolean>> futures = new ArrayList<RFuture<Boolean>>(acquiredLocks.size());
for (RLock rLock : acquiredLocks) {
RFuture<Boolean> future = rLock.expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS);
futures.add(future);
}
for (RFuture<Boolean> rFuture : futures) {
rFuture.syncUninterruptibly();
}
}
return true;
}
lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);这步是最底层的方法,一直深入进去可以看到如下代码:
下面的字符串就是redis命令。有三个参数
-
KEYS[1]就是Collections.singletonList(getName()),表示分布式锁的key,即REDLOCK_KEY;
-
ARGV[1]就是internalLockLeaseTime,即锁的租约时间,默认30s;
-
ARGV[2]就是getLockName(threadId),是获取锁时set的唯一值,即UUID+threadId:
<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.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.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
4,释放锁
@Override
public void unlock() {
unlockInner(locks);
}
5,用法
//准备
List<RLock> rLockList = new ArrayList<>();
for (RedissonClient client : clients) {
rLockList.add(client.getLock(key));
}
RedissonRedLock redLock = new RedissonRedLock(rLockList.toArray(转数组));
//获取锁
redLock.tryLock(waitTime, leaseTime, unit);
//业务逻辑
try{
//你的业务逻辑
} catch () {
} finally {
//保证一定会释放锁
redLock.unlock(key)
}
6,实现原理
https://mp.weixin.qq.com/s/JLEzNqQsx-Lec03eAsXFOQ
antirez提出的redlock算法大概是这样的:
在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。我们确保将在N个实例上使用与在Redis单实例下相同方法获取和释放锁。现在我们假设有5个Redis master节点,同时我们需要在5台服务器上面运行这些Redis实例,这样保证他们不会同时都宕掉。
为了取到锁,客户端应该执行以下操作:
-
获取当前Unix时间,以毫秒为单位。
-
依次尝试从5个实例,使用相同的key和具有唯一性的value(例如UUID)获取锁。当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试去另外一个Redis实例请求获取锁。
-
客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
-
如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
-
如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)
公司只用了3个redis,可重入锁获取2个及以上就说明获取锁成功