实现分布式锁
分布式锁其实就是,控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。
- 「互斥性」: 任意时刻,只有一个客户端能持有锁。
- 「锁超时释放」:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。
- 「可重入性」:一个线程如果获取了锁之后,可以再次对其请求加锁。
- 「高性能和高可用」:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。
- 「安全性」:锁只能被持有的客户端删除,不能被其他客户端删除
核心思想
借助redis 的SET key value[EX seconds][PX milliseconds][NX|XX]
命令的原子性实现分布式锁获取
- NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取。
- EX seconds :设定key的过期时间,时间单位是秒。
- PX milliseconds: 设定key的过期时间,单位为毫秒
- XX: 仅当key存在时设置值
📌1. set命令要用
set key value px milliseconds nx
2. value要具有唯一性
3. 释放锁时要验证value值,不能误解锁
代码实现
// 当前处理业务ID
String orderId = "orderId-123456";
// 当前请求唯一ID
String requestId = UUID.randomUUID().toString(true) + Thread.currentThread().getId();
// 锁自动过期时间, 100s避免死锁
long expire = 100L;
// 当key不存在时才会set,成功返回true
boolean lock = Boolean.TRUE.equals(
redisTemplate.opsForValue().setIfAbsent(orderId, requestId, Duration.ofSeconds(expire))
);
if (lock) {
try {
// 拿到锁
log.info("执行业务逻辑");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 释放锁
// 非原子性处理(不可靠):判断是不是当前线程加的锁,是才释放。
// if (requestId.equals(String.valueOf(redisTemplate.opsForValue().get(orderId)))){
// // 如果此时锁已到期自动失效,可能会误删别人的锁
// redisTemplate.delete(orderId);
// }
// 原子性处理方法(可靠):使用lua脚本
String lua = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end;";
Long res = redisTemplate.execute(
new DefaultRedisScript<>(lua, Long.class), Collections.singletonList(orderId), requestId
);
log.info(Long.valueOf(1).equals(res)? "解锁成功": "解锁失败");
}
}
注意上述分布式锁实现仍然存在问题:假设线程a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完,锁已自动过期销毁。此时线程b请求过来,就可以成功获得锁,也开始执行临界区的代码
基于Redisson框架实现
该框架就可以解决上述提到的问题,只要线程一加锁成功,redisson就会启动一个watch dog
看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。
核心内容
设置多个Redis master部署,以保证它们不会同时宕掉。并且这些master节点是完全相互独立的,相互之间不存在数据同步。同时,需要确保在这多个master实例上,是与在Redis单实例,使用相同方法来获取和释放锁
- 按顺序向N个master节点请求加锁
- 根据设置的超时时间来判断,是不是要跳过该master节点1
- 如果大于等于N/2+1个节点加锁成功,并且获取N个锁使用的总时间小于锁的有效期,即可认定加锁成功
- 如果加锁失败,则全部解锁
相关案例
Redlock:Redis分布式锁最牛逼的实现 (qq.com)
客户端设置网络连接和响应超时时间,并且超时时间要小于锁的失效时间。(假设锁自动失效时间为10秒,则超时时间一般在5-50毫秒之间,我们就假设超时时间是50ms吧)。如果超时,跳过该master节点 ↩︎