分布式锁是控制分布式系统间同步访问共享资源的一种方式。如果不同的系统或同一个系统的不同主机之间共享了同一个资源,那么访问这些资源的时候,需要使用互斥的手段来防止彼此之间的干扰,以保证一致性,这种情况就需要使用分布式锁。
一个基础的分布式锁应具备以下特性:
- 互斥:在任意一个时刻,只有一个客户端持有锁;
- 无死锁:即便持有锁的客户端崩溃或者出现网络分区,锁仍然可以被获取;
- 容错:只要大部分集群节点都活着,客户端就可以获取和释放锁。
常见的分布式锁的实现方案有以下 3 种:
- 基于数据库,如 MySQL;
- 基于缓存,如 Redis;
- 基于Zookeeper、etcd 等。
从性能的角度考虑,自然是缓存 > Zookeeper、etcd > 数据库,但是通常我们还要结合实际应用场景来决定采用哪种方案,因此以上 3 种方案我们都有必要了解下。
基于数据库
基本实现
首先,我们需创建“资源锁表”,类似以下表结构:
CREATE TABLE `resource_lock` (
`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`resource_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '资源',
`description` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '描述',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `resource_id_index` (`resource_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='资源锁表';
然后,我们要获取某个资源的锁,只需插入一条记录:
INSERT INTO `resource_lock` (`resource_id`, `description`) VALUES ('xx', 'xxxx');
之后,我们要释放某个资源的锁,删除对应的记录即可:
DELETE FROM `resource_lock` WHERE `resource_id` = 'xx';
原理
由于我们对 resource_id 字段添加了唯一性约束,这样如果同一资源同时有多个请求提交到数据库的话,数据库就可以保证只有一个操作能够成功(其它的会报错:Duplicate entry 'xx' for key 'resource_lock.resource_id_index'),因此我们就可以认为操作成功的那个请求获得了锁。
不足与改进
上述实现存在以下几点不足:
- 锁没有失效机制:一旦释放锁(删除记录)失败就会导致锁记录一直在数据库中,其它线程无法再获得锁;
- 锁是非阻塞的:获取锁(插入记录)失败后会直接报错;
- 锁不具备可重入性:同一个线程在没有释放锁之前无法再次获得锁,因为数据库中已经存在同一条记录了。
针对以上不足,我们可以做出以下改进:
- 在表中增加“过期时间”字段,并添加定时任务,定时清理过期的记录;
- 添加 while 循环,直到获取锁(插入记录)成功后,才终止循环并返回成功;
- 在表中增加“主机信息”、“线程信息”等字段,当线程再次获取锁时,先查询并判断记录中的主机信息和线程信息是否与当前线程的一致,如果一致就直接分配锁。
最终实现
CREATE TABLE `resource_lock` (
`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`resource_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '资源标识',
`host` varchar(50) NOT NULL COMMENT '主机',
`thread_id` varchar(50) NOT NULL COMMENT '线程ID',
`expire_time` datetime NOT NULL COMMENT '过期时间(为空表示永不过期)',
`description` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '描述',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `resource_id_index` (`resource_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='资源锁表';
致命缺陷
在实际应用中,我们很少会基于数据库来实现分布式锁,这是因为数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库还需要双机部署、数据同步、主备切换。简而言之,就是性能低下,部署麻烦。
基于 ZooKeeper
由于 ZooKeeper 天生设计定位就是分布式协调,强一致性,因此它的锁的模型健壮、简单易用,很适合做分布式锁。另外,如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小。
思路
使用临时顺序 znode 来表示获取锁的请求,创建最小后缀数字 znode 的用户成功拿到锁。
避免羊群效应(herd effect)
把锁请求者按照后缀数字进行排队,后缀数字小的锁请求者先获取锁。如果所有的锁请求者都 watch 锁持有者,当代表锁请求者的 znode 被删除以后,所有的锁请求者都会通知到,但是只有一个锁请求者能拿到锁。这就是羊群效应。
为了避免羊群效应,每个锁请求者 watch 它前面的锁请求者。每次锁被释放,只会有一个锁请求者会被通知到。这样做还让锁的分配更具有公平性,锁的分配遵循先到先得的原则。
步骤
- 创建一个目录 mylock;
- 线程 A 想获取锁就在 mylock 目录下创建临时顺序节点;
- 获取 mylock 目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
- 线程 B 获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
- 线程 A 处理完,删除自己的节点,线程 B 监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
简单示例
- 终端 1 创建节点:create -e /lock;
- 终端 2 创建节点:create -e /lock,此时会提示:Node already exists: /lock;
- 终端 2 watch 节点:stat -w /lock;
- 终端 1 关闭:quit;
- 终端 2 创建节点:create -e /lock,由于终端 1 会释放掉锁,因此能创建成功。
Curator 实现
Curator 是 Netflix 公司开源的一个 Zookeeper 客户端,与 Zookeeper 提供的原生客户端相比,Curator 的抽象层次更高,简化了 Zookeeper 客户端编程。
/**
* 模拟一次只能由一个线程访问的某些外部资源
*/
@Slf4j
public class FakeLimitedResource {
private final AtomicBoolean inUse = new AtomicBoolean(false);
public void use() {
// in a real application this would be accessing/manipulating a shared resource
if (!inUse.compareAndSet(false, true)) {
throw new IllegalStateException("Needs to be used by one client at a time");
}
try {
Thread.sleep(3 * new Random().nextInt(1));
} catch (InterruptedException e) {
log.error("Sleep with exception", e);
} finally {
inUse.set(false);
}
}
}
/**
* 资源调用者
*/
@Slf4j
public class ResourceInvoker {
/**
* 公共受限资源
*/
public FakeLimitedResource resource;
/**
* 分布式锁
*/
private InterProcessMutex lock;
/**
* 获取锁之前等待的时间(单位:秒)
*/
private int waitSeconds;
public ResourceInvoker(FakeLimitedResource resource, InterProcessMutex lock, int waitSeconds) {
this.resource = resource;
this.lock = lock;
this.waitSeconds = waitSeconds;
}
public void invoke() throws Exception {
String threadName = Thread.currentThread().getName();
if (waitSeconds <= 0) {
lock.acquire();
} else if (!lock.acquire(waitSeconds, TimeUnit.SECONDS)) {
throw new IllegalStateException("Thread[" + threadName + "] could not acquire the lock");
}
try {
log.info("Thread[{}] had the lock", threadName);
resource.use();
} finally {
log.info("Thread[{}] releasing the lock", threadName);
// always release the lock in a finally block
lock.release();
}
}
}
/**
* 分布式锁使用示例
*/
public class LockingExample {
private final CuratorFramework client;
public LockingExample(CuratorFramework client) {
this.client = client;
}
public void execute() throws InterruptedException {
FakeLimitedResource resource = new FakeLimitedResource();
InterProcessMutex lock = new InterProcessMutex(client, "/examples/lock");
ResourceInvoker invoker = new ResourceInvoker(resource, lock, 3);
int poolSize = 5;
int repetitions = poolSize * 10;
ExecutorService service = new ThreadPoolExecutor(
poolSize,
poolSize,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(),
new CustomThreadFactory("lock"));
Callable<Void> task = () -> {
try {
for (int j = 0; j < repetitions; ++j) {
invoker.invoke();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
};
for (int i = 0; i < poolSize; ++i) {
service.submit(task);
}
service.shutdown();
service.awaitTermination(10, TimeUnit.MINUTES);
}
}
优缺点
优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
缺点:因为需要频繁的创建和删除节点,性能上不如 Redis 方式。
基于 Redis
简单实现
setnx lock:codehole true
... do something critical ...
del lock:codehole
如上,可以很简单,但是有个问题,就是如果逻辑执行过程中出现异常了,可能会导致 del 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。
因此我们需要在拿到锁之后,再给锁加上一个过期时间,比如 5s,这样即使过程中出现异常,也可以保证 5s 之后锁会自动释放。
setnx lock:codehole true
expire lock:codehole 5
... do something critical ...
del lock:codehole
但是以上逻辑还有问题。如果在 setnx 和 expire 之间服务进程突然挂掉了(可能是因为机器断电或被人为中止),就会导致 expire 得不到执行,这样也会造成死锁。
这种问题的根源就在于 setnx 和 expire 是两条指令而不是原子指令,所以我们应该使用这两个指定的原子指令。
set lock:codehole true ex 5 nx
... do something critical ...
del lock:codehole
可重入性
Redis 分布式锁如果要支持可重入,需要对客户端的 set 方法进行包装,使用线程的 Threadlocal 变量存储当前持有锁的计数。
不推荐使用可重入锁,它加重了客户端的复杂性,在编写业务方法时注意在逻辑结构上进行调整完全可以不使用可重入锁。
超时问题
Redis 分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行时间太长,以至于超出了锁的超时限制,就会出现问题。因为这时候第一个线程持有的锁过期了,临界区的逻辑还没有执行完,这个时候第二个线程就提前重新持有了这把锁,导致临界区代码不能得到严格的串行化执行。
为了避免这个问题,Redis 分布式锁不要用于较长时间的任务。如果真的偶尔出现了,数据出现的小波错乱可能需要人工介入解决。
有一个稍微安全一点的方案是设置较长的过期时间,并为 set 指令的 value 参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后再删除 key,这是为了确保当前线程占有的锁不会被其它线程释放,除非这个锁时过期了被服务器自动释放的。但是匹配 value 和删除 key 不是一个原子操作,Redis 也没有提供类似于 delifequals 这样的指令,这就需要使用 Lua 脚本来处理了,因为 Lua 脚本可以保证连续多个指令的原子性执行。(之所以要保证原子性,是为了避免当前线程误删除其它线程的 key,因为在匹配 value 和删除 key 两个操作中间,有可能当前线程持有的锁正好过期,然后被其它线程获取到。)
# delifequals
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
但是这也不是一个完美的方案,它只是相对安全一点,因为如果真的超时了,当前线程的逻辑没有执行完,其它线程也会乘虚而入。
使用 Redisson
Redisson 实现了分布式和可扩展的 Java 数据结构,促使使用者对 Redis 的关注分离,提供很多分布式相关操作服务,例如分布式锁、分布式集合、可通过 Redis 实现延迟队列等,从而让使用者能够将精力更集中地放在处理业务逻辑上。
为了彻底解决超时问题,我们可以在获取锁成功之后,开启一个定时守护线程,每隔一段时间(小于锁的过期时间)检查锁是否还存在,存在则对锁的过期时间延长,从而避免锁过期提前释放。
上述思想,在 Redisson 里称之为 Watch Dog 机制,下图演示了 Redission 的加锁机制:
在使用 Redisson 时,需要注意以下事项:
- Reddison 默认加锁时长为 30 秒,每隔 10 秒(1/3)检查一次,如果存在就重新设置过期时间为 30 秒;
- 可以通过设置 lockWatchdogTimeout 参数来调整 Redisson 的默认加锁时长;
- 只有在使用不带过期参数的 lock 方法时,Redisson 才会启用 Watch Dog 机制;(如果在调用带过期参数的 lock 方法时开启 Watch Dog 机制,有可能会造成线程不安全)
- 如果释放锁操作本身出现异常,Watch Dog 不会再续期锁,因此可以避免死锁的发生;
- Redisson 锁默认是非公平锁,同时也提供了公平锁、读写锁的实现。
另外,在集群环境下,还需要考虑主从发生 failover 的场景(详见下节),为此 Redisson 还提供了 RedLock 版本的锁的实现,但是该实现已经被废弃了,取而代之的方案是 Redisson 的所有操作都会等待传播到所有 Redis 从节点,以此提高故障转移期间的可靠性。
使用 Redlock
在集群环境下,前面的分布式锁实现是有缺陷的,它不是绝对安全的。
比如在 Sentinel 集群中,主节点挂掉时,从节点会取而代之,客户端上却并没有明显感知。原先第一个客户端在主节点中申请成功了一把锁,但是这把锁还没有来得及同步到从节点,主节点突然挂掉了。然后从节点变成了主节点,这个新的节点内部没有这个锁,所以当一个客户端过来请求加锁时,立即就批准了。这样就会导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生。
不过这种不安全也仅仅是在主从发生 failover 的情况下才会产生,而且持续时间极短,业务系统多数情况下可以容忍。
为了解决这个问题,Redis 提供了 Redlock。要使用 Redlock,需要提供多个 Redis 实例,这些实例之间相互独立没有主从关系。同很多分布式算法一样,Redlack 也使用“大多数机制”。
使用 Redlock 也是有代价的,需要更多的 Redis 实例,性能也下降了,代码上还需要引入额外的 library,运维上也需要特殊对待,这些都是需要考虑的成本。
要了解更多,可以阅读 Redis分布式锁。
使用 RedisTemplate
不考虑锁的超时与集群环境,我们也可以使用 RedisTemplate 来实现分布式锁:
@Slf4j
public class RedisLock {
/**
* 每次获取锁失败后,线程下次获取锁前要等待的时间
*/
private static final long DEFAULT_SLEEP_MILLS = 100;
/**
* 释放锁的 Lua 脚本,保证匹配校验与释放锁这两个动作的原子性
*/
public static final String UNLOCK_SCRIPT = "if redis.call(\"get\",KEYS[1]) == ARGV[1] " +
"then return redis.call(\"del\",KEYS[1]) else return 0 end";
private final RedisTemplate<String, Object> redisTemplate;
public RedisLock(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 获取锁,如果未获取成功,线程将一直阻塞
* 如果线程阻塞时发生中断,重置中断状态,并继续等待获取锁
*
* @param key 锁标识
*/
public void lock(String key) {
lock(key, -1, DEFAULT_SLEEP_MILLS);
}
/**
* 获取锁,如果未获取成功,线程将一直阻塞
* 如果线程阻塞时发生中断,重置中断状态,并继续等待获取锁
*
* @param key 锁标识
* @param leaseTime 获取锁后持有锁的最大时间(如果未通过调用 unlock释放锁)。如果 leaseTime 为 -1,则持有锁直到显式解锁为止。
*/
public void lock(String key, long leaseTime) {
lock(key, leaseTime, DEFAULT_SLEEP_MILLS);
}
/**
* 获取锁,如果未获取成功,线程将一直阻塞
* 如果线程阻塞时发生中断,重置中断状态,并继续等待获取锁
*
* @param key 锁标识
* @param leaseTime 获取锁后持有锁的最大时间(如果未通过调用 unlock释放锁)。如果 leaseTime 为 -1,则持有锁直到显式解锁为止。
* @param sleepMills 每次获取锁失败后,线程下次获取锁前要等待的时间
*/
public void lock(String key, long leaseTime, long sleepMills) {
long threadId = Thread.currentThread().getId();
long waitMillis = 0;
while (!Objects.equals(leaseTime == -1 ?
redisTemplate.opsForValue().setIfAbsent(key, threadId) :
redisTemplate.opsForValue().setIfAbsent(key, threadId, Duration.ofMillis(leaseTime)), true)) {
try {
Thread.sleep(sleepMills);
} catch (InterruptedException e) {
log.warn("thread has been interrupted while trying get lock", e);
Thread.currentThread().interrupt();
}
waitMillis += sleepMills;
log.info("get lock: {} failed, thread-id: {}, already wait millis: {}", key, threadId, waitMillis);
}
}
/**
* 尝试获取锁
* 如果在线程阻塞时发生中断,则重置中断状态,并继续等待获取锁
*
* @param key 锁标识
* @return 是否获取成功
*/
public boolean tryLock(String key) {
return tryLock(key, -1, -1, DEFAULT_SLEEP_MILLS);
}
/**
* 尝试获取锁
* 如果在线程阻塞时发生中断,则重置中断状态,并继续等待获取锁
*
* @param key 锁标识
* @param leaseTime 获取锁后持有锁的最大时间(如果未通过调用 unlock释放锁)。如果 leaseTime 为 -1,则持有锁直到显式解锁为止。
* @return 是否获取成功
*/
public boolean tryLock(String key, long leaseTime) {
return tryLock(key, leaseTime, -1, DEFAULT_SLEEP_MILLS);
}
/**
* 尝试获取锁
* 如果在线程阻塞时发生中断,则重置中断状态,并继续等待获取锁
*
* @param key 锁标识
* @param leaseTime 获取锁后持有锁的最大时间(如果未通过调用 unlock释放锁)。如果 leaseTime 为 -1,则持有锁直到显式解锁为止。
* @param waitTime 等待获取锁的最大时间。如果超时后仍未获取到锁,就返回 false。如果 waitTime 为 -1,则获取锁失败后将直接返回 false。
* @return 是否获取成功
*/
public boolean tryLock(String key, long leaseTime, long waitTime) {
return tryLock(key, leaseTime, waitTime, DEFAULT_SLEEP_MILLS);
}
/**
* 尝试获取锁
* 如果在线程阻塞时发生中断,则重置中断状态,并继续等待获取锁
*
* @param key 锁标识
* @param leaseTime 获取锁后持有锁的最大时间(如果未通过调用 unlock释放锁)。如果 leaseTime 为 -1,则持有锁直到显式解锁为止。
* @param waitTime 等待获取锁的最大时间。如果超时后仍未获取到锁,就返回 false。如果 waitTime 为 -1,则获取锁失败后将直接返回 false。
* @param sleepTime 每次获取锁失败后,线程下次获取锁前要等待的时间。
* @return 是否获取成功
*/
public boolean tryLock(String key, long leaseTime, long waitTime, long sleepTime) {
long threadId = Thread.currentThread().getId();
long waitMillis = 0;
do {
Boolean success = leaseTime == -1 ?
redisTemplate.opsForValue().setIfAbsent(key, threadId) :
redisTemplate.opsForValue().setIfAbsent(key, threadId, Duration.ofMillis(leaseTime));
if (Objects.equals(success, true)) {
log.info("get lock: {} successful, thread-id: {}", key, threadId);
return true;
}
if (waitTime == -1) {
break;
}
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
log.warn("thread has been interrupted while trying get lock", e);
Thread.currentThread().interrupt();
}
waitMillis += sleepTime;
log.info("get lock: {} failed, thread-id: {}, already wait millis: {}", key, threadId, waitMillis);
} while (waitMillis < waitTime);
return false;
}
/**
* 释放锁
* <p>
* 为确保当前线程占有的锁不被其他线程释放,需要在执行 set 命令(获取锁)时将 value 设置为一个唯一值,
* 之后在释放锁前先判断 value 是否一致,如果一致才执行 delete 命令(释放锁)。
* <p>
* 由于匹配 value 与 delete key 不是原子操作,因此需要使用 Lua 脚本来保证连续多个指令的原子性。
*
* @param key 锁标识
* @return 是否获取成功
*/
public boolean unLock(String key) {
long threadId = Thread.currentThread().getId();
RedisScript<Long> redisScript = RedisScript.of(UNLOCK_SCRIPT, Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), threadId);
boolean success = result != null && result > 0;
log.info("unlock: {} {}, thread-id: {}", key, success ? "successful" : "fail", threadId);
return success;
}
/**
* 尝试批量获取锁
*
* @param keys 锁标识集合
* @param leaseTime 获取锁后持有锁的最大时间(如果未通过调用 unlock释放锁)。如果 leaseTime 为 -1,则持有锁直到显式解锁为止。
* @return 是否获取成功
*/
public boolean tryLocks(List<String> keys, long leaseTime) {
long threadId = Thread.currentThread().getId();
Map<Boolean, List<String>> map = keys.stream()
.collect(Collectors.partitioningBy(key -> {
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, threadId, Duration.ofMillis(leaseTime));
return Objects.equals(success, true);
}));
List<String> failedKeys = map.get(false);
if (CollectionUtils.isEmpty(failedKeys)) {
log.info("try get locks: {} successful, thread-id: {}", keys, threadId);
return true;
}
failedKeys.forEach(this::unLock);
return false;
}
/**
* 批量释放锁
*
* @param keys 锁标识集合
* @return 是否成功标识
*/
public boolean unLocks(List<String> keys) {
long threadId = Thread.currentThread().getId();
Map<Boolean, List<String>> map = keys.stream().collect(Collectors.partitioningBy(key -> {
RedisScript<Long> redisScript = RedisScript.of(UNLOCK_SCRIPT, Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), threadId);
return result != null && result > 0;
}));
List<String> failedKeys = map.get(false);
if (CollectionUtils.isEmpty(failedKeys)) {
log.info("unlock: {} successful, thread-id: {}", keys, threadId);
return true;
}
log.error("unlock: {} failed, failed keys: {}, thread id: {}", keys, failedKeys, threadId);
return false;
}
/**
* 尝试获取锁,如果成功获取到锁,则执行 callable,否则抛出异常
* 如果在线程阻塞时发生中断,则重置中断状态,并继续等待获取锁
*
* @param key 锁标识
* @param runnable 成功获取到锁后,要执行的动作。
* @return 是否获取成功
*/
public void tryExecute(String key, Runnable runnable) {
tryExecute(key, -1, -1, runnable);
}
/**
* 尝试获取锁,如果成功获取到锁,则执行 callable,否则抛出异常
* 如果在线程阻塞时发生中断,则重置中断状态,并继续等待获取锁
*
* @param key 锁标识
* @param leaseTime 获取锁后持有锁的最大时间(如果未通过调用 unlock释放锁)。如果 leaseTime 为 -1,则持有锁直到显式解锁为止。
* @param runnable 成功获取到锁后,要执行的动作。
* @return 是否获取成功
*/
public void tryExecute(String key, long leaseTime, Runnable runnable) {
tryExecute(key, leaseTime, -1, -1, runnable);
}
/**
* 尝试获取锁,如果成功获取到锁,则执行 callable,否则抛出异常
* 如果在线程阻塞时发生中断,则重置中断状态,并继续等待获取锁
*
* @param key 锁标识
* @param leaseTime 获取锁后持有锁的最大时间(如果未通过调用 unlock释放锁)。如果 leaseTime 为 -1,则持有锁直到显式解锁为止。
* @param waitTime 等待获取锁的最大时间。如果超时后仍未获取到锁,就返回 false。如果 waitTime 为 -1,则获取锁失败后将直接返回 false。
* @param runnable 成功获取到锁后,要执行的动作。
* @return 是否获取成功
*/
public void tryExecute(String key, long leaseTime, long waitTime, Runnable runnable) {
tryExecute(key, leaseTime, waitTime, -1, runnable);
}
/**
* 尝试获取锁,如果成功获取到锁,则执行 callable,否则抛出异常
* 如果在线程阻塞时发生中断,则重置中断状态,并继续等待获取锁
*
* @param key 锁标识
* @param leaseTime 获取锁后持有锁的最大时间(如果未通过调用 unlock释放锁)。如果 leaseTime 为 -1,则持有锁直到显式解锁为止。
* @param waitTime 等待获取锁的最大时间。如果超时后仍未获取到锁,就返回 false。如果 waitTime 为 -1,则获取锁失败后将直接返回 false。
* @param sleepTime 每次获取锁失败后,线程下次获取锁前要等待的时间。
* @param runnable 成功获取到锁后,要执行的动作。
* @return 是否获取成功
*/
public void tryExecute(String key, long leaseTime, long waitTime, long sleepTime, Runnable runnable) {
if (tryLock(key, leaseTime, waitTime, sleepTime)) {
try {
runnable.run();
} catch (Exception e) {
throw new IllegalCallerException(e);
} finally {
unLock(key);
}
} else {
throw new RedisLockAcquireException(MessageFormat.format("获取 Redis 分布式锁:{} 失败", key));
}
}
/**
* 尝试获取锁,如果成功获取到锁,则执行 callable,否则抛出异常
* 如果在线程阻塞时发生中断,则重置中断状态,并继续等待获取锁
*
* @param key 锁标识
* @param callable 成功获取到锁后,要执行的动作。
* @return 是否获取成功
*/
public <T> T tryExecute(String key, Callable<T> callable) {
return tryExecute(key, -1, -1, callable);
}
/**
* 尝试获取锁,如果成功获取到锁,则执行 callable,否则抛出异常
* 如果在线程阻塞时发生中断,则重置中断状态,并继续等待获取锁
*
* @param key 锁标识
* @param leaseTime 获取锁后持有锁的最大时间(如果未通过调用 unlock释放锁)。如果 leaseTime 为 -1,则持有锁直到显式解锁为止。
* @param callable 成功获取到锁后,要执行的动作。
* @return 是否获取成功
*/
public <T> T tryExecute(String key, long leaseTime, Callable<T> callable) {
return tryExecute(key, leaseTime, -1, -1, callable);
}
/**
* 尝试获取锁,如果成功获取到锁,则执行 callable,否则抛出异常
* 如果在线程阻塞时发生中断,则重置中断状态,并继续等待获取锁
*
* @param key 锁标识
* @param leaseTime 获取锁后持有锁的最大时间(如果未通过调用 unlock释放锁)。如果 leaseTime 为 -1,则持有锁直到显式解锁为止。
* @param waitTime 等待获取锁的最大时间。如果超时后仍未获取到锁,就返回 false。如果 waitTime 为 -1,则获取锁失败后将直接返回 false。
* @param callable 成功获取到锁后,要执行的动作。
* @return 是否获取成功
*/
public <T> T tryExecute(String key, long leaseTime, long waitTime, Callable<T> callable) {
return tryExecute(key, leaseTime, waitTime, -1, callable);
}
/**
* 尝试获取锁,如果成功获取到锁,则执行 callable,否则抛出异常
* 如果在线程阻塞时发生中断,则重置中断状态,并继续等待获取锁
*
* @param key 锁标识
* @param leaseTime 获取锁后持有锁的最大时间(如果未通过调用 unlock释放锁)。如果 leaseTime 为 -1,则持有锁直到显式解锁为止。
* @param waitTime 等待获取锁的最大时间。如果超时后仍未获取到锁,就返回 false。如果 waitTime 为 -1,则获取锁失败后将直接返回 false。
* @param sleepTime 每次获取锁失败后,线程下次获取锁前要等待的时间。
* @param callable 成功获取到锁后,要执行的动作。
* @return 是否获取成功
*/
public <T> T tryExecute(String key, long leaseTime, long waitTime, long sleepTime, Callable<T> callable) {
if (tryLock(key, leaseTime, waitTime, sleepTime)) {
try {
return callable.call();
} catch (Exception e) {
throw new IllegalCallerException(e);
} finally {
unLock(key);
}
} else {
throw new RedisLockAcquireException(MessageFormat.format("获取 Redis 分布式锁:{} 失败", key));
}
}
}
技术选型
通过前面的分析,要实现分布式锁,我们可以基于 ZooKeeper 或者 Redis,它们各有千秋,那么要如何选型呢?
- 如果是追求高性能,采用 Redis 实现;
- 如果是强调安全性,采用 ZooKeeper 实现。
个人倾向于基于 ZooKeeper 的实现,因为基于 Redis 的实现是有可能存在隐患的。
因此,如果公司里面有 ZooKeeper 集群条件,优先选用 ZooKeeper,否则的话,采用 Redis 实现也可以。
另外,还有可能是系统已经引入了 Redis,同时又不想再引入其它外部依赖,那么也可以采用 Redis 实现。
标准使用范式
最后,在使用锁时还应遵循标准的编程范式,以使用前面的 RedisLock 为例:
// 使用 tryLock
if (redisLock.tryLock(lockKey, timeout, TimeUnit.HOURS, waitTime)) {
try {
// TODO do business
} finally {
redisLock.unlock(lockKey);
}
} else {
throw new RuntimeException("系统繁忙,请稍后重试");
}
// 使用 lock
redisLock.lock(lockKey, timeout, TimeUnit.HOURS)
try {
// TODO do business
} finally {
redisLock.unlock(lockKey);
}
更多细节参见 Lock 标准使用范式