一起养成写作习惯!这是我参与「掘金日新计划 · 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。
解决
通过缓存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命令。