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