事故总结集锦-11 redis set不更新数据 导致的COE(一周一更)

1,030 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情

背景

【问题描述】

  • 通过分布式redis缓存版本号,控制巨量的MQ消费顺序,也是业界内经常用的方式。此次事故的问题也是出现在这里:商品库存修改操作,每次操作都会使用redis,更新set最新的缓存版本号,发送MQ,下游消费库存mq来修改库存数量,但是消费的时候还是出现了混乱,导致库存不准,造成线上库存超卖、少卖、状态更新异常等问题。

【事故级别】

  • P0

【过程】

  • 接到商家反馈,库存修改数量改成0,但是系统查出来却是100,导致超卖
  • 排查到问题根源在于MQ消费错乱导致
  • 使用了分布式redis set命令缓存了每次操作的版本号,但是有时候没生效,没有写进去
  • 排查了set方法,确认了问题根源所在

【故障原因】

问题出在:Boolean set (String key, String value, long timeout, TimeUnit unit, final boolean exist) 的exist参数。

原因分析

底层调⽤的是jimdb的set⽅法,使⽤的是set多参数⽅法,exist传⼊的是false,

  • 代表当缓存中key不存在时写⼊成功
  • 如果key存在,则返回false,放弃更新数据,以致在有效期内,数据版本version得不到实时更新,最终MQ消费顺序混乱。

源代码模拟

public void setValue(String key, String field, long timeOut) {
    redisUtil.set(key, field, timeOut,TimeUnit.SECONDS,false);
}

这里会导致 我们在修改完库存后,如果在规定timeOut内,又修改了一次的话,最新的版本号并不会写入到缓存里。所以版本号就会一直是同一个,版本号完全失去了作用,导致下游消费mq的系统,消费顺序错乱。而且,版本号加超时时间,这个操作很是令人费解!纯正的bug。

image.png

解决

通过缓存hIncrBy方法实现分布式的版本号累加,从而确保下游消费mq的顺序性。

public Long doIncrement(String key, String filed, int value) {
    return redisUtil.hIncrBy(key, filed, value);
}

命令详解

set多参数:

Boolean set(String key, String value, long timeout, TimeUnit unit, final boolean exist)

exist =true的情况


set(key, value, timeout, unit, true) ## key 存在才覆盖 并设置过期时间

底层: set key value ex timeout xx ## ex设置过期时间 xx key存在才覆盖

验证:

redis-> get abc

(nil)

redis-> set abc 123 ex 100 xx    ex设置过期时间 xx key存在才覆盖

(nil)

redis-> get abc  校验key不存在未写⼊成功

(nil)

redis-> set abc 456  写⼊key不过期

OK

redis-> set abc 123 ex 100 xx

OK

redis-> get abc key存在,可以设置成功

"123"

redis-> ttl abc  过期时间会进⾏重写

(integer) 91

exist =false的情况

set(key, value, timeout, unit, false) ## key 不存在才写⼊ 并设置过期时间

底层: set key value ex timeout nx ## ex设置过期时间 nx key不存在才写⼊

验证:

redis-> get abc ## key不存在
(nil)
redis-> set abc 123 ex 100 nx
## 设置不存写⼊123
OK
redis-> get abc
"123"
redis-> set abc 456 ex 800 nx
## 再次操作
(nil)
redis-> get abc ## key存在导致数据不更新
"123"
redis-> ttl abc ## 过期时间未被覆盖
(integer) 81

总结

本次事故的核心原因,源于对set多参数命令的不了解,所以在使用前一定要充分的了解命令的含义。另外一个原因可能也看到了,redisUtil的工具封装类,很多原生方法被封装,本意是好的,封装的姿势是不对的,在封装的时候要透传源生的方法参数,不能参杂自身的业务属性在内,比如写死:exist =false。相信调用这个方法的同学,在set基础上肯定还要加锁,针对value版本号进行+1,远不如hIncrBy命令。