秒杀踩坑记:库存超卖

本案例发生在别人身上,以为有学习借鉴的意义特转载过来记录一下。

PM 说有一个相似于抢购的小需求,咱们第一反应就想到是典型的防止库存超卖场景,因而理所因当地选用了 Redis 方案。只要保证是原子操做,便可防止库存超卖,天然想到使用 Incr/Decr 这类原子操做。html

查看 PHP 的 Redis 扩展关于 Incr 方法的说明:redis

/**
 * 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 {
    //很差意思呢,已经被抢完了
}

不知道你有没有闻到这段代码的坏味道,在大部分状况下会如你所想地运行,可是特殊场景下会 出现判断失效 的逻辑问题,例如:异步

一、key 因为某些缘由失效了;
二、Incr 操做失败了,不会抛异常并返回 false;ide

上述两种状况,都会致使$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 未能成功漂移,这部分是运维的锅,研发代码不够健壮,这锅一样要背 >﹏<。日志

优化方案

首先,修改代码使其更加健壮,增长计数器容错处理:code

$num = $redis->incr($key);
if ($num > 0 && $num < $max) {
    //入抢购成功队列,异步去执行抢购成功逻辑
} else {
    //很差意思呢,已经被抢完了
}

而后,切换 Redis 源到高可用集群(Codis),测试并从新上线,第二日的抢购已经正常,看着 Cat 上流量逐渐平稳,内心也踏实了。

查看来源

相关文章
相关标签/搜索