记一次分布式锁失效的生产事故
项目背景:
在给某项目做业务开发的时候,有一个任务派发的定时任务,该定时任务通过算法,把系统源源不断的任务每隔一分钟派发给不同的审核员进行审核。因为考虑到分布式任务调度器(如:xxljob/elasticjob)需要单独服务器搭建服务,所以为了减少服务器成本,就自己基于现有资源编写分布式锁,因为现有redis环境,于是就基于redis编写分布式锁,来保证多个实例只有一台实例在同一个时间点在执行派发任务。
资源说明:
redis主从模式,1主3从,主从只做备份,不做读写分离。服务:三个实例,定时任务采用自己编写的分布式锁,要求某一时刻只能有一台实例执行定时任务。
事故还原:
原分布式锁的代码贴图如下:
由图中代码可以看出,出问题的代码那么就是Redis设置锁的.setIfAbsent()这个过程中,导致if中的boolean值判断出错,且这个现象只会偶然发生的,一天一个实例执行 24小时 * 60(每分钟每个实例启动一次定时任务)= 1440次,可能会发生一次或几次分布式锁出错,也可能不发生。生产(三个实例)上线小半个月,发生了一次(结果在发现问题的第二天又发生了两次)。
boolean success = Boolean.TRUE.equals(redisTemplate.opsForValue() .setIfAbsent(redisKey, value, LOCK_TIME, TimeUnit.SECONDS));
在网上查看了很多说法,大致意思是:基于Redision的API来实现分布式锁,在使用.setIfAbsent()这个方法的时候可能存在返回 null 的情况。但我寻思着,即使返回为 null ,那么在Boolean.TRUE.equals(null)的结果也应该是false,这样同样会进入if条件,表明该实例设置锁没有成功,不会继续向下执行任务。
即使我知道不是.setIfAbsent()这个方法返回 null 的情况引起的bug,我还是按照网上对null进行了判断,但是过了两天还是出现了问题,同样是redis分布式锁没有锁住(且只有某一次没有锁住,因为每一分钟在三台实例都会执行一次,我查看了其他时间然后横向横向对比,不常发生但是可能存在发生的风险)
其中ip为72的实例setIfAbsent()成功,在2022-11-14 15:43:00:007 执行了数据总量查询
其中ip为73的实例setIfAbsent()成功,在2022-11-14 15:43:00:017 执行了数据总量查询
两次执行前后相差大概10ms,但是两个都获取到了分布式锁,都往下执行了数据总量查询和日志打印:
// 查询数据总数,用于分批处理 Integer totalCount = subProgramExamineRecordService.countDistributeData(taskDistributeQueryDTO); log.info("[人审任务分配]-查询待分配的数据重量:{}条,当前时间:{}", totalCount, LocalDateTime.now());
下图源码截图分析:
其他时段redis分布锁没有问题的部分截图
如果单就if()中的条件判断没有问题,那么唯一出问题的地方就是redisTemplate.opsForValue().setIfAbsent(),即底层存在.setIfAbsent()前后两次设置同一个key都可能返回成功的情况。
验证猜想:
于是我带着上面的疑问编写一下面一段代码,使用三个线程模拟三个实例去竞争分布式锁,只有抢到分布式锁的实例才能执行业务逻辑,结果就验证了我上面的猜想,在进行分布式锁的时候,在极少数的概率下,多个实例可能同时竞争到一个分布式锁。
先看代码:
while循环大概跑了10分钟,大概跑了几千次三个实例并发挣钱分布式锁,出现了一次两个实例同时获取到了分布式锁,结束了while循环
运行结果截图:
自此验证结果完毕,问题也找到了,就是在并发情况下,存在极小的概率执行redisTemplate.opsForValue().setIfAbsent()都能设置分布式锁成功(即多个实例可以获取到同一个分布式锁)。
解决办法:
代码贴图如下: