秒殺踩坑記:庫存超賣

本案例發生在別人身上,覺得有學習借鑑的意義特轉載過來記錄一下。

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 上流量逐漸平穩,心裏也踏實了。

查看來源

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章