上篇博客我简答介绍了 redis 常见加解锁的操作,本篇我计划整理出其中可能导致的问题:
一般情况下 redis 分布式锁都是这样使用的:
// 加锁操作
cacheService.lock(key , timeout);
// 业务逻辑
xxx
// 解锁操作
cacheService.unlock();
绝大多数情况下都没有问题,但可能存在如下情况:业务逻辑执行时间过长,锁自动失效了。如果锁失效,就有可能导致并发问题
一般情况下都是通过设置 timeout 值大于业务逻辑执行时间来解决,但总得来说这不是一个长久之计,因为随着业务逻辑的扩张,业务逻辑执行的速度可能随着业务数据的增多而变慢,如果一直动态调整 timeout 值的话,每次上线改一个参数,实际是大多数人不愿意看到的。
下面我列举出几种常见的解决方案:
1、timeout 设置为 -1,直接不过期不就行了,只有执行完业务逻辑,主动解锁下一个实例才能执行
首先这种方案肯定是不行的,假设业务逻辑执行过程中出现异常,没有执行解锁操作怎么办,那岂不是说其它实例永远无法获取分布式锁。此时你可能想说通过 finally 把解锁逻辑括起来,即使出错也解锁。那万一不是异常,而是系统直接挂了呢,finally 只能保证实例正常的情况下执行,万一本身应用已经挂了呢,此时一个实例挂了,其它等待 redis 分布式锁的实例也无法正常执行
2、通过守护线程续命,如果发现执行业务逻辑期间锁快过期了,就给它续一定的时长
使用这种方案时,实例加锁可以用当前加锁时间戳作为 value 存储,守护线程定期根据时间戳判断 value 是否快过期,如果快过期就给它延长一段时间
3、超时回滚
使用这种方案时,每个实例必须包含独立的 value 值,以该值做为分布式锁的值。每次解锁时判断当前占用锁的线程还是否自己,如果是正常执行,否则回滚,因为分布式锁执行超时
对于方案二和方案三,一旦真的出现续命或者超时回滚情况,一定要考虑 redis 过期时间是否设置的合理,适当延长时间,尽可能不要人为参与到这种加解锁的逻辑当中
最后我们再简单这块很可能存在一种异常场景:
- 实例 a 抢占到分布式锁,设置 value 及过期时长
- 此时该 key-value 保存在 redis master 节点,突然 master 节点挂了
- redis 集群从 slave 节点中推选出新的 master
- 由于异步同步的原因,新的 master 此时不包含该分布式锁 key-value
- 实例 b 抢占到分布式锁,设置 value 及过期时长
此时实例 a 和 实例 b 都抢占到 redis 锁,就有可能造成同步问题
实际该问题的本质是 redis 异步同步可能丢数据的问题,只是用到分布式锁这个模块比较重要,影响范围更大。此时一般有两种解决方式:
- RedLock 获取分布式锁
- Zookeeper 实现分布式锁
这里简单介绍下 RedLock 的逻辑:
- 获取当前时间,以毫秒为单位
- 客户端依次从5个毫无关系的 redis 实例请求分布式锁
- 通过当前时间减去步骤1的时间计算获取锁的时长,当且仅当 (N / 2 + 1)个 redis 实例获取锁成功并且获取锁的时间小于锁失效的时间才算获取锁成功
此时如果获取锁成功,锁真正有效时间等于初始有效时间(根据步骤1时间 + 过期时长算出的时间)减去真正获取到锁的时间。如果获取锁失败执行解锁逻辑
总的来说 RedLock 实施难度太大,成本高是一方面,各个 redis 实例时间还得对其,还得防止不出现重启等情况。一般很少使用,了解即可