同步秒殺實現:Redis在秒殺功能的實踐

於玉桔發表於2019-01-18

回顧目錄

Redis在資源秒殺場景中的使用

業務概述

  • 秒殺資源:以周為時長的資源。
  • 每個頁面都會有秒殺資源,數量在1~8份,以隨機形式展示給訪客。
  • 每週秒殺資源價格由資料部門計算定價,沒有有一個時間點進行搶購,如:每週三10點。購買者搶購數量可以是 秒殺資源剩餘資源中的任意數量。
  • 購買者是否有搶購秒殺資源的許可權,由使用者介面資訊,賬戶資訊,等許可權介面等決定。
  • 購買者支付方式使用介面支付,系統生成購買者搶購支付加密資訊,跳轉支付頁面,再支付介面後,非同步回掉確定是否購買成功,如果購買失敗需要及時退回秒殺資源庫存,以供他人報買。

業務流程

時間軸 業務 流程節點備註
週一 生成資源資料 流程①
週三10:00前 check資源資料 流程②
週三10:00 購買者秒殺秒殺資源 流程③
週三10:00後 購買者退款 流程④
週日 本週資源搶購結束,生成外網展示資訊 流程⑤

Redis節點說明

  • 通用redis:用於SSO做統一登入、以及非秒殺功能使用。
  • 快取redis:用於儲存購買者熱身資料,搶購這查詢資訊的快取。
  • 核心redis:負責資源庫存剩餘數量,秒殺秒殺資源搶佔等核心業務實現,需要關閉redis的lru策略,程式控制記憶體中key的淘汰

Redis使用詳情

  • 快取redis-資料熱身 流程①② (牛奶供給降級策略)

    • 關鍵虛擬碼

      cacheRedis.setex(key,EXPIRE_TIME_7D,info);
      複製程式碼
    • 秒殺qps峰值在1w左右,但是超過60%的qps請求的是查詢列表方法,所以需要增加可購買秒殺資源快取。

    • 關鍵虛擬碼

      生成rediskey, objects包括ucid、使用者輸入入參、分頁資訊等等
      public static String builder(String prefix, Object... objects) {
          String input = JSONObject.toJSONString(Arrays.asList(objects));
          String output = Util.md5_16(input);
          return prefix+output;
      }
      cacheRedis.setex(key,EXPIRE_TIME_2S,info);
      複製程式碼
    • 設計優點:借鑑spring-data-redis將入參通用為objects...序列化,然後將JsonString Md5壓縮為16位,這裡主要由於在秒殺開始時,redis資料會出現大量快取列表資料,redis儲存100w個value長度為32位,key長度為16位的資料時,需要使用個130MB記憶體,如果key的長度為32位時需要160MB左右的記憶體,所以壓縮key的長度在這種場景很有必要。

  • 核心redis-秒殺資源秒殺 流程③

    • 每個秒殺資源擁有自己的佇列,完成多佇列,低佇列長度的秒殺。

    • 關鍵虛擬碼

      String key = PURCHASING_PRODUCT + productId;
      Long count = coreRedis.llen(key);
      判斷count是否大於庫存
      判斷count+使用者欲購買秒殺資源數量(share)是否大於庫存
      
      String[] values = (uuid+uid) * share; 
      
      if (inventory - coreRedis.lpush(key, values)) < 0) {
          coreRedis.lrem(key, share, values);
      }
      
      例如:id:1 秒殺資源有3份流量的庫存, 
      當llen時發現秒殺資源在redis中沒有資料,
      購買者20xxxxx1想買此資源3份流量,
      這時lpush後發現超賣,lrem退回庫存。
      redis 127.0.0.1:6379> lrange XX_PRODUCT_1 0 -1
      1) "jali7xz20xxxxx1"
      2) "3whsh6b20xxxxx2"
      3) "3whsh6b20xxxxx2"
      4) "3whsh6b20xxxxx2"
      複製程式碼
    • 設計優點:核心命令llen、lpush的時間複雜度都是O(1)、lrem時間複雜度是O(N),官方lrem給出的複雜度是O(N)但我覺得在這種使用場景下lrem的複雜度應該無極限接近於O(count),但是將補償操作封裝為原子性,且支援多次、冪等執行。曾經也想過用一些getset,setnx,pipelin、將庫存快取到佇列然後pop、事務等實現秒殺。但是效能、或者魯棒性在這種場景下都沒有以上設計表現出色,而且這種方式在支付失敗,或者查詢到未支付的情況下立刻冪等lrem秒殺資源佇列的訂單,其他有資格購買的購買者可以繼續購買。

Redis線上使用情況

  • 快取redis (圖片來源地址:github)

cache redis

  • 核心redis

cache redis

Redis使用總結

  • 使用一主一從,rdb為備份策略的redis架構,QPS在8W以下是沒有任何問題的(第一期秒殺資源秒殺,在沒有做redis多庫負載切分,以及沒有優化使用的情況下到了5W的QPS,沒有出現超時連結,或者獲取不到連線池資源的情況,也和沒有使用事務以及採用的低複雜度命令實現有關
  • 像列表頁快取,切勿為了減少redis的開銷,將資料庫每一列放到redis中,在redis中查詢彙總,例如:每個秒殺資源都放在redis中,秒殺資源頁需要10次redis連結才能完成一次列表頁的組裝。這樣做會將伺服器的qps成幾何倍數的擴大到與redis的qps中造成系統獲取不到redis連線資源。
  • 如果redis只用作快取資料,且追求極限效能,master可以關閉記憶體快照和日誌記錄,有slave節點完成。
  • 批量命令降低QPS可使用Redisson、Lettuce或lua指令碼代替。

相關文章