1、概述
在多线程的环境下,为了保证一个代码块在同一时间只能由一个线程访问,Java中我们一般可以使用 synchronized
语法和 ReentrantLock
去保证,这实际上是本地锁的方式。而在如今分布式架构的热潮下,如何保证不同节点的线程同步执行呢?
实际上,对于分布式场景,我们可以使用分布式锁,分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用。
分布式锁的特点
-
「互斥性:」 同一时刻只能有一个线程持有锁。
-
「可重入性:」 同一节点上的同一个线程如果获取了锁之后能够再次获取锁。
-
「锁超时:」 类似于J.U.C中的锁,支持锁超时,以防止死锁。
-
「高性能和高可用:」 加锁和解锁需要高效,并且需要保证高可用性,防止分布式锁失效。
-
「具备阻塞和非阻塞性:」 能够及时从阻塞状态中被唤醒。
2、Redis粗糙实现
Redis本身可以被多个客户端共享访问,是一个共享存储系统,适合用来保存分布式锁。由于Redis的读写性能高,可以应对高并发的锁操作场景。
Redis的SET
命令有一个NX
参数,可以实现「key不存在才插入」,因此可以用它来实现分布式锁:
-
如果key不存在,则表示插入成功,可以用来表示加锁成功;
-
如果key存在,则表示插入失败,可以用来表示加锁失败;
-
当需要解锁时,只需删除对应的key即可解锁成功;
-
为了避免死锁,需要设置合适的过期时间。
这样描述,我们可以得到一个十分粗糙的分布式锁实现。
// 尝试获得锁
if (setnx(key, 1) == 1){
// 获得锁成功,设置过期时间
expire(key, 30)
try {
//TODO 业务逻辑
} finally {
// 解锁
del(key)
}
}
然而,上述实现方式存在一些问题,使其不能被称为合格的分布式锁:
-
「非原子性操作:」 多条命令的操作不是原子性的,可能会导致死锁的产生。
-
「锁误解除:」 存在锁误解除的可能性,即在持有锁的线程在内部出现阻塞时,锁的TTL到期导致自动释放,而其他线程误解除锁的情况。
-
「业务超时自动解锁导致并发问题:」 由于业务超时自动解锁,可能导致并发问题的发生。
-
「分布式锁不可重入:」 实现的分布式锁不支持重入。
3、解决遗留问题
3.1误删情况
在以下情况下可能会出现误删情况:
-
持有锁的线程1在锁的内部出现了阻塞,导致其锁的TTL到期从而锁自动释放;
-
此时线程2尝试获取锁,由于线程1已经释放了锁,线程2可以拿到;
-
但是随后线程1解除阻塞,继续执行并开始释放锁&#x