记一次分布式锁失效的生产事故

项目背景

  在给某项目做业务开发的时候,有一个任务派发的定时任务,该定时任务通过算法,把系统源源不断的任务每隔一分钟派发给不同的审核员进行审核。因为考虑到分布式任务调度器(如:xxljob/elasticjob)需要单独服务器搭建服务,所以为了减少服务器成本,就自己基于现有资源编写分布式锁,因为现有redis环境,于是就基于redis编写分布式锁,来保证多个实例只有一台实例在同一个时间点在执行派发任务。

 

资源说明

  redis主从模式,1主3从,主从只做备份,不做读写分离。服务:三个实例,定时任务采用自己编写的分布式锁,要求某一时刻只能有一台实例执行定时任务。

 

事故还原:  

  原分布式锁的代码贴图如下:

  

  由图中代码可以看出,出问题的代码那么就是Redis设置锁的.setIfAbsent()这个过程中,导致if中的boolean值判断出错,且这个现象只会偶然发生的,一天一个实例执行 24小时 * 60(每分钟每个实例启动一次定时任务)= 1440次,可能会发生一次或几次分布式锁出错,也可能不发生。生产(三个实例)上线小半个月,发生了一次(然后第二天又发生了两次),查看日志文件,实例A在14:48:00秒开启了定时任务,整个定时任务结束实在14:

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()都能设置分布式锁成功(即多个实例可以获取到同一个分布式锁)。

  

解决办法:

  代码贴图如下:

 

posted @ 2022-11-17 16:54  颜子歌  阅读(700)  评论(5编辑  收藏  举报