本案例发生在别人身上,觉得有学习借鉴的意义特转载过来记录一下。
PM 说有一个类似于抢购的小需求,我们第一反应就想到是典型的防止库存超卖场景,于是理所因当地选用了 Redis 方案。只要保证是原子操作,即可防止库存超卖,自然想到使用 Incr/Decr 这类原子操作。
查看 PHP 的 Redis 扩展关于 Incr 方法的说明:
/**
* Increment the number stored at key by one.
*
* @param string $key
* @return int the new value
* @link http://redis.io/commands/incr
*
*/
public function incr( $key ) {}
可见,Incr 方法返回的是 key 操作后的新值,即 ++1 后的值,于是我们写出了如下代码:
$num = $redis->incr($key);
if ($num < $max) {
//入抢购成功队列,异步去执行抢购成功逻辑
} else {
//不好意思呢,已经被抢完了
}
不知道你有没有闻到这段代码的坏味道,在大部分情况下会如你所想地运行,但是特殊场景下会 出现判断失效 的逻辑问题,例如:
1、key 由于某些原因失效了;
2、Incr 操作失败了,不会抛异常并返回 false;
上述两种情况,都会导致$num < $max条件成立,进而导致更严重的逻辑问题,最终超卖。
问题描述与分析
我们就抢购开始后就遇到了上述的第二种情况,下面描述整个过程。先通过 Cat 监控平台观察到访问量急剧上升,开始担心应用服务坑不住,随后日志平台报警 Incr 操作存在异常几率,再然后就出现超卖情况,紧急情况只能关闭业务开关。是什么原因导致判断条件成立?
通过日志定位到 Incr 操作问题,便 Telnet 连接到线上 Redis 服务,发现了异常情况:
/# 查看值
GET key
100
# 尝试修改
INCR key
READONLY You can't write against a read only slave
INFO
# Replication
role:slave
可以看出来,该连接的机器目前处于从机状态,不可写操作,所以 Incr 操作返回 false,同时 PHP 不同类型比较会存在隐式转化,所以false < $num恒成立,导致计数器失效。而这一切又是由于 Redis 高可用不完善,当主从切换后,VIP 未能成功漂移,这部分是运维的锅,研发代码不够健壮,这锅同样要背 >﹏<。
优化方案
首先,修改代码使其更加健壮,增加计数器容错处理:
$num = $redis->incr($key);
if ($num > 0 && $num < $max) {
//入抢购成功队列,异步去执行抢购成功逻辑
} else {
//不好意思呢,已经被抢完了
}
然后,切换 Redis 源到高可用集群(Codis),测试并重新上线,第二日的抢购已经正常,看着 Cat 上流量逐渐平稳,心里也踏实了。
查看来源
原创文章,作者:carmelaweatherly,如若转载,请注明出处:https://blog.ytso.com/193475.html