「黑马点评」五、分布式锁优化
基于 SETNX 的分布式锁问题
- 不可重入:获得锁的线程可以再次进入到相同锁的代码块中,意义在于防止死锁,如在锁内调用另一个带锁的,产生死锁
- 不可重试:没有重试机制,才获取一次就返回 false
- 超时释放:锁然避免了死锁,但是如果是业务超时,锁自动释放,可能的额外风险
- 主从一致性:如果使用 Redis 的主从集群,因为异步同步,可能会出现主节点宕机,数据未同步
Redisson 入门
Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid) > 提供了一系列的分布式的 Java 常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现
引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.23.1</version>
</dependency>
配置类
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("123321");
return Redisson.create(config);
}
}
获取锁:lock.tryLock(waitTime, leasetime, timeUnit)
,空参为默认 30s 释放,仅获取一次
释放锁:lock.unlock()
业务代码使用
Long userId = UserHolder.getUser().getId();
RLock lock = redissonClient.getLock("lock:order:" + userId);
boolean isLock = lock.tryLock();
if (!isLock) {
return Result.error("不允许重复下单");
}
try {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
lock.unlock();
}
Redisson 可重入锁
用信号量一样的形式对锁计数,核心就是利用哈希结构存储锁的线程以及次数

image.png|500
要保证原子性则必用 Lua 脚本
获取锁的 Lua 脚本

image.png|500
释放锁的 Lua 脚本

image.png|500
超时释放时间没传则为 -1,但在调用时会从 watchDog 处获取默认值为 30s
pttl
、pexpire
毫秒级别
Redisson 分布式锁流程

image.png|500
这里的看门狗是用来给为锁续有效期的,一旦宕机无法续约便自行释放
- 可重入:利用 hash 结构记录线程 id 和重入次数
- 可重试:利用信号量和 Publish & Subscribe 功能实现等待、唤醒,获取锁失败的重试机制
- 超时续约:利用 watchDog,每隔 leaseTime/3 重置过期时间
Redisson 分布式锁主从一致性问题
问题:获取锁刚成功,主节点就宕机了,此时被选为主节点的从节点并未同步完数据,锁失效

image.png|500
解决:使用多个主节点,只有当所有主节点都获取锁成功,那才是真正的成功
此时如果担心出现宕机,可以为每个从节点添加字节点,这时,从节点被选为主节点,哪怕有一个线程趁虚而入,因为无法获取全部节点的锁,所以也无法加锁成功(前提多个主节点没有全挂)

image.png|500
使用
RLock lock1 = redissonClient.getLock("lock:order:" + userId);
RLock lock2 = redissonClient.getLock("lock:order:" + userId);
RLock lock3 = redissonClient.getLock("lock:order:" + userId);
RLock lock = redissonClient.getMultiLock(lock1, lock2, lock3);
TODO:就这样吧,以后再研究源码分析
分布式锁总结
- 不可重入 Redis 分布式锁
- 原理:利用 SETNX 的互斥性;利用 EX 避免死锁;利用线程标示检测并释放锁
- 缺陷:不可重入,无法重试、锁超时失效
- SETNX 直接放 key,如果存在返回 null 此时获取锁失败;如果不存在则放入成功;key 存在代表锁存在;删除 key 代表释放锁;无法重入,并且有超时误删的情况
- 可重入 Redis 分布式锁
- 原理:利用 HASH 结构记录线程标示以及重入次数;利用 watchDog 自动续时;利用信号量控制锁重试等待
- 缺陷:Redis 宕机导致锁失效
- Redisson 的 getLock,基本方法 tryLock 和 unlock;可以设置获取锁的等待时间、锁的持有时间、时间单位;利用 HASH 结构所以可以重入,重入一次就给 value 加 1;看门狗机制是为超时误删准备的,不设置锁的持有时间,默认为 -1,在内部实现中是 30s,每过 30s/3=10s 就会再次修正过期时间为 30s;当其他线程竞争锁会被阻塞在等待时间内,并且会订阅消息,一旦释放锁就发送释放消息,阻塞的线程就会来竞争锁;如果在主从场景下,刚获取锁成功,主节点还没同步完消息就宕机了,那锁就失效了
- Redisson 的 multiLock
- 原理:利用多个独立的 Redis 节点,当全部主节点获取到重入锁,才算获取锁成功
- 缺陷:运维成本高、实现复杂
- 多个 redisson 的 lock 联合起来,getMultiLock(lock1, lock2, lock3),每个锁代表了不同的 Redis 服务器;要获取锁成功就要每个节点都上锁成功;某个节点宕机就无法上锁,可以再加一层从节点;redLock 与 multiLock 差不多,超过一半节点返回才是获取锁成功