日常开发中,基于 Redis 天然支持分布式锁,大家在线上分布式项目中都使用过 Redis 锁。本文主要针对日常开发中加锁过程中某些异常场景进行讲解与分析。本文讲解示例代码都在 https://github.com/wayn111/newbee-mall-pro 项目 test
目录下 RedisLockTest
类中。
版本声明:
Spring Boot
版本 3.0.2- 演示项目地址:https://github.com/wayn111/newbee-mall-pro
- github地址:http://github.com/wayn111 欢迎大家关注,点个star
一、任务超时,锁已经过期
这个异常场景说实话发生概率很低,大部分情况下加锁时任务执行都会很快,锁还没到期,任务自己就会删除锁。除非说任务调用第三方接口不稳定导致超时、数据库查询突然变得非常慢就可能会产生这个异常场景。
那怎么处理这个异常嘞?大部分人可能都会回答添加一个定时任务,在定时任务内检测锁快过期时,进行续期操作。OK,这么做好像是可以解决这个异常,那么博主在这里给出自己的见解。
1.1 先说一个暴论:如果料想到有这类异常产生,为什么不在加锁时,就把加锁过期时间设置大一点
不管所续期还是增大加锁时长,都会导致一个问题,其他线程会迟迟获取不到锁,一直被阻塞。那结果都一样,为什么不直接增大加锁时间?
想法是好的,但是实际上,加锁时间的设置是我们主观臆断的,我们无法保证这个加锁代码的执行时间一定在我们的锁过期时间内。作为一个严谨的程序员,我们需要对我们的代码有客观认知,任务执行可能几千上亿万次都是正常,但就是那么一次它执行超时了,可能由于外部依赖、当前运行环境的异常导致。
1.2 直接不设置过期时间,任务不执行完,不释放锁
如果在加锁时就不设置过期时间的话,理论上好像是可以解决这个问题,任务不执行完,锁就不会释放。但是作为程序员,总觉得哪里怪怪的,任务不执行完,锁就不会释放!
仔细想想,我们一般在 try 中进行加锁 在 finally 进行锁释放,这个好像也没毛病哦。但是实际针对一些极端异常场景下,如果任务执行过程中,服务器宕机、程序突然被杀掉、网络断连等都可能造成这个锁释放不了,另一个任务就一直获取不到锁。
这个方案程序正常的情况下,可以满足我们的要求,但是一旦发生异常将导致锁无法释放的后果,也就是说只要我们解决这个锁在异常场景下无法释放的问题,这个方案还是OK的。博主这里直接给出方案:
在不设置过期时间的加锁操作成功时,给一个默认过期时间比如三十秒,同时启动一个定时任务,给我们的锁进行自动续期,每隔 默认过期时间 / 3
秒后执行一次续期操作,发生锁剩余时长小于 默认过期时间 / 2
就重新赋值过期时长为三十秒。这样的话,可以保证锁必须由任务执行完才能释放,当程序异常发生时,仍然能保证锁会在三十秒内释放。
1.3 设置过期时间,任务不执行完,不释放锁
这个方案本质上与方案二的解决方案相同,还是启动定时任务进行续期操作,流程这里不做多余讲述。需要注意的就是加锁指定过期时间会比较符合我们的客观认知。实际上他的底层逻辑跟方案二相同,无非就是定时任务执行间隔,锁剩余时长续期判断要根据过期时间来计算。
综合来看:方案三会最合适,符合我们的客观认知,跟我们之前对 Redis 的使用逻辑较为相近。
二、线程B加锁执行中未释放锁,线程A释放了线程B的锁
说实话我仔细思考了一下这个异常场景,发现这个异常是个伪命题,如果线程 B 正在执行时,线程 A 怎么能获取到线程B的锁!线程 A 获取不到线程 B 的锁,谈何来去释放线程 B 的锁!如果线程 A 能获取到线程 B 的锁那么这个分布式锁的代码一开始就已经错了。
这里回到这个异常场景本身,我们可以给每个线程设置请求ID,加锁成功将请求ID设置为加锁 key 的对应 value,线程释放锁时需要判断当前线程的请求ID与
加锁 key 的对应 value 是否相同,相同则可以释放锁,不相同则不允许释放。
三、线程加锁成功后继续申请加锁
这个场景主要发生在加锁代码内部调用栈过深,比如说加锁成功执行方法 a,在方法 a 内又重复申请了同一把锁,导致线程把自己锁住了,这个业界的主流叫法是叫锁的可重入性。
解决方式有两种,一是修改方法内的加锁逻辑,不要加同一把锁,修改方法 a 内的加锁 key 名称。二是针对加锁逻辑做修改,实现可重入性。
这里简单介绍如何实现可重入性,给每个线程设置请求ID,加锁成功将请求ID设置为加锁 key 的对应 value,针对同一个线程的重复加锁,判断当前线程已存在请求ID的情况下,请求ID直接与加锁 key 的对应 value 相比较,相同则直接返回加锁成功。
四、 代码实践
4.1 加锁自动续期实践
设置锁过期时间为10秒,然后该任务执行15秒,代码如下:
ps: 以下代码都可以在 https://github.com/wayn111/newbee-mall-pro 项目
test
目录下RedisLockTest
类中找到
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class RedisLockTest {
@Autowired
private RedisLock redisLock;
@Test
@Test
public void redisLockNeNewTest() {
String key = "test";
try {
log.info("---申请加锁");
if (redisLock.lock(key, 10)) {
// 模拟任务执行15秒
log.info("---加锁成功");
Thread.sleep(15000);
log.info("---执行完毕");
}
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
redisLock.unLock(key);
}
}
}
执行如下:
可以看出就算任务执行超过过期时间也能通过自动续期让代码正常执行。
4.2 多线程下其他线程无法共同申请到同一把锁实践
启动两个线程,线程 A 先加锁, 线程 B 后枷锁
@Test
public void redisLockReleaseSelfTest() throws IOException {
new Thread(() -> {
String key = "test";
try {
log.info("---申请加锁");
if (redisLock.lock(key, 10)) {
// 模拟任务执行15秒
log.info("---加锁成功");
Thread.sleep(15000);
log.info("---执行完毕");
} else {
log.info("---加锁失败");
}
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
redisLock.unLock(key);
}