什么是分布式锁?
分布式锁,顾名思义,就是分布式系统中使用的锁,在单体应用中我们使用synchronized、ReentrantLock来解决线程时间的共享资源的访问问题,而在分布式系统中,资源贡献问题已经由线程之间的竞争演变到了进程之间的竞争,分布式锁就是接近分布式系统中多进程之间的共享资源的访问问题。
分布式锁应该具备的特征:
- 多进程可见,且互斥。
- 具备高可用,高性能,具备良好的容错性。
- 非阻塞的模式,如果没有获取到锁,要及时返回获取锁失败。
- 具备锁失效机制,防止死锁的发生。
- 加锁和释放锁必须是同一个客户端。
分布式锁常见的实现方式:
- 基于MySQL实现分布式锁。
- 基于Redis实现分布式锁。
- 基于Zookeeper实现分布式锁。
MySQL、Redis、Zookeeper实现分布式锁的简单对比:
MySQL | Redis | Zookeeper | |
---|---|---|---|
加锁原理 | 使用数据库的锁机制 | 使用lua脚本实现 | 使用顺序节点+监听机制实现 |
性能 | 一般 | 好 | 一般 |
可用性 | 一般 | 好 | 好 |
安全性 | 好 | 主从模式会有安全性问题 | 好 |
实现难度 | 简单容易理解 | 有封装好的Redisson | 相对前两者没有优势 |
Redis分布式锁的实现原理?
- 使用setnx实现分布式锁,使用del命令来释放锁。
问题:获取锁成功后,在执行任务的时候,程序挂了,这个时候还没有执行del命令,就会产生死锁。
解决方法:给锁设置超时时间。 - 使用set lockKey lockValue nx ex expireTime 给锁设置超时时间。
问题:锁续期问题,因为有过期时间,如果任务还没有执行完,锁就过期了,其他线程会获取锁成功,等当前线程任务执行完了,释放了锁这时候释放的其他线程获取的锁。
解决方法:锁续期,开启一个守护线程,在锁将要过期的时候,对锁进行自动续期,同时在释放锁的时候判断是否是当前线程释放的(这里涉及非原子性操作)。 - Redisson实现分布式锁:Redisson分布式锁中解决了锁超时和加锁解锁不是同一个线程及非原子操作等一系列问题。
什么是Redisson?
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。
其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。
Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
Redisson实现分布式锁的原理?
- Redisson使用lua脚本来实现分布式锁,把所有操作都封装成一个lua脚本,保证了操作的原子性。
- Redisson在获取锁后,会启动一个看门狗watch dog 线程,它会对将要过期而任务没有执行完成的锁进行续期,注意如果Redisson加锁时候设置了过期时间,watch dog机制会失效,watch dog检查锁超时的默认时间是30秒。
Redisson实现分布式锁的主要特征:
- 通过lua脚本实现了加锁解锁的操作,保证了原子性。
- 使用了watch dog机制,watch dog会对将要过期的锁进行续期操作,避免了锁过期带来的问题。
- 记录了获取锁的客户端标志,每次加锁的时候进行判断,保证了锁的重入。
分布式锁加锁解锁简单流程:
代码简单演示:
@Autowired
private RedissonLock lock;
public Map<String, String> getApiLogTypeCollection() {
if (!CollectionUtils.isEmpty(cacheService.hGetAll(RedisKeyConstant.API_LOG_TPYE))) {
return cacheService.hGetAll(RedisKeyConstant.API_LOG_TPYE);
}
//重新缓存
try {
if (lock.tryLock(RedisKeyConstant.API_LOG_TPYE_LOCK, 2, 3)) {
if (!CollectionUtils.isEmpty(cacheService.hGetAll(RedisKeyConstant.API_LOG_TPYE))) {
return cacheService.hGetAll(RedisKeyConstant.API_LOG_TPYE);
}
//重新加载redis缓存
initApiLogType();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock(RedisKeyConstant.API_LOG_TPYE_LOCK);
}
return null;
}
注意释放锁一定要放在finally中。
使用Redis主从模式实现分布式锁有什么问题?
主从模式下,主节点挂掉时,从节点会取而代之,客户端上却并没有明显感知,第一个客户端在主节点中申请成功了一把锁,但是这把锁还没有来得及同步到从节点,主节点突然挂掉了,然后从节点变成了主节点,这个新的节点内部没有这个锁,所以当另一个客户端过来请求加锁时,立即就批准了,这样就会导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生,不过这种不安全也仅仅是在主从发生failover的情况下,而且持续时间极短。
什么是RedLock?
RedLock直译就是红锁,是用N相互独立的、没有主从关系的master节点,以保证他们大多数情况下不会同时宕机,N个一般取奇数个,客户端通过执行如下流程来获取锁。
- 想要获取锁的客户端轮流用相同的key,随机值在N个节点上请求,客户端在每个master上请求锁时,会有一个和锁的释放时间相比很小的超时时间,防止客户端在某个master节点阻塞过长时间,如果一个master节点挂了,尽快尝试下一个。
- 客户端计算第二步中获取锁花的时间,只有当客户端在超过一半的master节点获取了锁,而且总消耗时间不超过锁释放时间,就认为锁获取成功了。
- 如果锁获取成功了,那么锁自动释放时间就是最初的释放时间减去获取锁消耗的时间。
- 如果锁获取失败了,不管因为获取成功的锁是否超过半数还是因为总消耗超过了超时时间,客户端都会到每个master节点上释放锁,即使那些认为没有获取成功的锁;。
分布式锁的使用场景?
- 秒杀场景,避免超卖。
- 分布式缓存场景,避免多次刷缓存。
- 分布式事务,需要保证不同节点对共享资源的操作是互斥的。
- 分布式任务调度的时候(这里不讨论xxl-job等分布式调度框架),使用分布式锁保证只有一个节点可以调度成功。
如有不正确的地方请各位指出纠正。