redisson根据部署方式不同分为几种不同的模式:初始化时要选择适合自己的模式。
集群模式、云托管模式、单节点模式、哨兵模式、主从模式
redis分布式锁实现分两类:
- 单master节点的锁 即所有数据存在一个master节点上,可能是单节点模式、主从模式。
- 多master节点的锁 即数据是分开存的,槽可能分在不同的节点上。这种采用RedLock实现。
单master节点锁原理:
先判断加锁的key是否存在,不存在则创建key并加上客户端唯一ID和当前线程ID,让其值+1,再给key设置过期时间。存在则判断是否当前客户端的线程加锁的锁,是则继续+1,更新过期时间。解锁的时候也是判断是否当前客户端线程加锁的锁,是锁-1;为0时则删除锁,并在一个通道发布消息。然后当没设置自动释放锁时间时,会有一线程在锁释放前去不断刷新过期时间。默认是30秒,因为程序也不知道你到底要用多久,所以每隔10秒去刷新过期时间为30秒。这个线程叫看门狗。获取不到锁的线程会去订阅一个通道,然后循环尝试获取锁。直到有人释放锁,发布了该通道的消息。然后订阅了该通道的线程则会被唤起去尝试会去锁。
这种锁的高可用存在问题,单节点部署一旦master挂掉则无法加锁。主从部署的话,刚加锁后主节点挂调,从节点升级为主节点。走新主节点去加锁可能发现没有该锁导致重复加锁。因为主从复制存在延迟。
多master节点锁原理(redlock)其实和上面类似,只不过会去大多数的节点加锁后才算加锁成功,这样就算部分节点挂调也可以保持高可用。
我这里单节点模式,贴下初始化代码。
public static RedissonClient redissonClient() { Config config = new Config(); config.setCodec(new JsonJacksonCodec()); SingleServerConfig singleServer = config.useSingleServer(); singleServer.setAddress(prefixAddress("redis://xxx:6379")); singleServer.setPassword("xxx"); return Redisson.create(config); }
接下来看Redisson具体怎么实现:获取锁时就会选择获取的是具体什么锁。
这里有个很关键点就是利用发布订阅来,实现后续的通知。
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
//获取锁
Long ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
// 为空表示已经获取到锁
return;
}
//这里订阅对应这个锁的渠道 redisson_lock__channel:{zzy_test}
//在释放锁时会去发布这个通知,这里就会收到消息
RFuture<RedissonLockEntry> future = subscribe(threadId);
if (interruptibly) {
commandExecutor.syncSubscriptionInterrupted(future);
} else {
commandExecutor.syncSubscription(future);
}
try {
//获取不到则循环等待获取锁
while (true) {
ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
break;
}
// waiting for message
if (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 {
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
} finally {
unsubscribe(future, threadId);
}
}
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
if (leaseTime != -1) {
//这里设置了过期时间则没有下面的看门狗线程
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
//加锁核心代码
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
if (ttlRemaining == null) {
//这里获取之后会添加看门狗线程去不断维护过期时间,默认是30秒释放锁,这里10秒续命一次
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
这里就是加锁核心逻辑,打包给lua脚本去执行。
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return evalWriteAsync(getName(), 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(getName()), internalLockLeaseTime, getLockName(threadId));
}
//再往里面跟进去有个关键的点
//计算槽,redis一共有16000+个槽,看key对应的哪个槽,再按槽去得到加锁的节点信息
@Override
public MasterSlaveEntry getEntry(String name) {
int slot = calcSlot(name);
return getEntry(slot);
}
接下来看解锁核心逻辑:解锁就会发布指定消息到对应渠道redisson_lock__channel:{zzy_test},这样等待锁的线程讲被唤起,这样就串起来了。
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getName(), 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(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}