分布式锁之redisson
使用redisson的原因
- 比zk性能要好
- 比使用redis.setnx的优势
- 使用redis.setnx时,如果系统挂掉,会一直锁住
- redisson将ttl和set 通过lua脚本原子性批量执行
实现
Lock lock = redisson.getLock("test");
lock.lock();
源码
RLock锁API
public interface RLock {
//----------------------Lock接口方法-----------------------
/**
* 加锁 锁的有效期默认30秒
*/
void lock();
/**
* tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false .
*/
boolean tryLock();
/**
* tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,
* 在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
*
* @param time 等待时间
* @param unit 时间单位 小时、分、秒、毫秒等
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* 解锁
*/
void unlock();
/**
* 中断锁 表示该锁可以被中断 假如A和B同时调这个方法,A获取锁,B为获取锁,那么B线程可以通过
* Thread.currentThread().interrupt(); 方法真正中断该线程
*/
void lockInterruptibly();
//----------------------RLock接口方法-----------------------
/**
* 加锁 上面是默认30秒这里可以手动设置锁的有效时间
*
* @param leaseTime 锁有效时间
* @param unit 时间单位 小时、分、秒、毫秒等
*/
void lock(long leaseTime, TimeUnit unit);
/**
* 这里比上面多一个参数,多添加一个锁的有效时间
*
* @param waitTime 等待时间
* @param leaseTime 锁有效时间
* @param unit 时间单位 小时、分、秒、毫秒等
*/
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
/**
* 检验该锁是否被线程使用,如果被使用返回True
*/
boolean isLocked();
/**
* 检查当前线程是否获得此锁(这个和上面的区别就是该方法可以判断是否当前线程获得此锁,而不是此锁是否被线程占有)
* 这个比上面那个实用
*/
boolean isHeldByCurrentThread();
/**
* 中断锁 和上面中断锁差不多,只是这里如果获得锁成功,添加锁的有效时间
* @param leaseTime 锁有效时间
* @param unit 时间单位 小时、分、秒、毫秒等
*/
void lockInterruptibly(long leaseTime, TimeUnit unit);
}
RedissonLock - tryAcquireAsync
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
// 如果过期时间不为空,则直接调用tryLockInnerAsync
if (leaseTime != -1) {
return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
//如果过期时间为空,则取internalLockLeaseTime时间,默认为30s
//tryLockInnerAsync会通过lua脚本执行hincrby pttl等命令
//redissonlock 采用的是hash结构,外层的key为创建RedissonLock时指定的name
//里层的key为线程id
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
if (ttlRemaining == null) {
//在该锁成功后,将该线程id添加至ExpirationEntry
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
RedissonLock - renewExpiration - 定时续期,防止程序未执行完,redis过期解锁
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
// 每1/3过期时间执行一次任务
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
//获取到第一个线程id
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
// 根据线程id续期
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
if (res) {
// reschedule itself
renewExpiration();
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
如果在redisson该续期时stw了,如何解决
忽略
redisson是在剩余过期时间超过1/3时续期,通常过期时间都会以s为单位,而stw时间非常短,以ms为单位,故不会出现上面的问题
RedissonRedLock
RedLock也不能解决gc的问题,只能能过多个无任何关联关系的redis服务器解决服务端挂掉的问题
zk
zk临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。注意,这里提到的是会话失效,而非连接断开。另外,在临时节点下面不能创建子节点。
这里还要注意一件事,就是当你客户端会话失效后,所产生的节点也不是一下子就消失了,也要过一段时间,大概是 10 秒以内,可以试一下,本机操作生成节点,在服务器端用命令来查看当前的节点数目,你会发现客户端已经 stop,但是产生的节点还在。
看上去通过 Zookeeper 实现分布式锁还是比较好的一种解决方案,但是它是完美的吗?从上面的分布式锁的流程可知,客户端线程想要获取锁就需要创建临时节点,这个时候客户端和 Zookeeper 之间就会维护一个 session,来表示该客户端还在排队等待获取锁。因此这个方案的潜在问题就在于一旦出现网络异常,或者客户端发生 STW GC,那么就可能导致 session 关闭,从而导致临时节点被关闭,此时就会出现原来客户端持有的锁被删除了,如果有另外的客户端过来加锁的话可以成功获取,那么此时就出现并发安全问题了。因此在这种极端条件下,Zookeeper 的分布式锁实现方案也不是 100%保证安全的。