天天看點

k碼特權中關于 Redis & MySQL DB 樂觀鎖應用

1.【背景】

斐訊路由App 需要新增k碼特權子產品。

2.【需求】

1.已認證k碼激活狀态驗證的使用者可免費領取k碼特權商品

2.每個使用者每天隻能領取一張k碼特權獎品

3.【應用場景及難點分析】

1.接口資料安全性要求:
1.1 當某k碼特權商品資料量為1,且高并發情況下,
1.2 如何防止超賣(即多個使用者都搶到了剩餘的一個商品)
           
2.接口性能要求:
斐訊路由App 現使用者量為300w+,日活4w+,2/8原則分析(**指80%的業務量在20%的時間裡完成**)。
經驗可知使用者使用斐訊路由App 的持續時間為12小時,
是以2/8分析後,80%的日活在20%的時間内完成。
即32000人免費領取k碼特權商品要在2.4小時内完成,
換算成每秒 完成請求數即 QPS = 3.7/s 。
即每個接口響應請求時間至少要在 270ms 以内。才算是高性能。
           

4.問題分析:

1.讀多寫少
每個使用者每日隻能領取一個k碼特權商品。即1個使用者加入請求免費領取k碼特權接口多次,在k碼商品庫存量充足的情況下,隻能領取到1個商品,其餘請求都應該傳回“對不起,您今日已領取k碼特權商品”。從這個方面來定義,其屬于讀多寫少的問題。
           
2.并發量低
以斐訊路由現在日活情況為4w+的資料量來估算、接口并發能力 QPS = 3.7/s ,
屬于低并發,但在k碼特權子產品優化程度達到一定量時,并發量是否會上升有待考察。
但總體來說屬于并發量不高的場景。
           

也就是說k碼特權問題經過模型抽象,已經變成了讀多寫少、并發量不大,但要保證性能,和資料安全性一緻性的問題。

對于這類問題,樂觀鎖思想可以作為解決這類問題的指導思想。

5.樂觀鎖思想

網上文章對樂觀鎖了解的誤區:

1.樂觀鎖是一種思想,并不是一種具體的技術實作。

2.樂觀鎖類似于CAS無鎖程式設計技術(其實也加鎖,隻不過在cpu層面)

即當多個線程同時并發更新統一個變量,
采用先select再update的方式,select出目前變量a的副本值b,然後用新值c去更新,
更新時需要拿select 出來的變量值a的副本值b與目前非副本變量a的值做對比,
若暫存副本值b與目前變量a非副本值相同,則正常更新,
如果不同,則認為在目前線程更新之前已經有一個值将a變量更新,
則更新失敗,在并發情況不大的情況下,
采用循環的方式去更新,總能更新成功,且循環更新次數不會太多。是以CAS也叫自旋鎖。
           

6.k碼特權-免費領取解決方案

1.使用者每日成功領取k碼特權商品次數的限制

采用redis 資料結構 String,記錄使用者每日免費領取成功次數。并且可以輕松使用redis 緩存的過期 (expire) 機制做每日領次數的控制),使用者每日成功領取k碼特權的次數次日淩晨清空。

為什麼不使用資料庫來進行使用者成功領取k碼特權商品次數的控制。當然建立好索引此問題也可以完美解決。

使用redis進行使用者成功領取k碼特權商品次數的控制原因有兩個:

1.因為redis 純粹的查詢快,減輕資料庫壓力!不用每次都通過資料庫二次索引,從磁盤找到目标記錄并讀入到記憶體。**
2.線上配置的redis使用量10%都不到,為了更好的利用硬體資源。**
           

2.使用者每日成功領取k碼特權次數的并發更改

從接口安全性考慮,若有使用者惡意領取、那麼有可能産生一個使用者在一天之内領取了多個k碼特權獎品,這個是業務需求所不允許的。

這裡我們使用到了redis 提供的 事務(multi)與watch(樂觀鎖實作) 機制來控制 使用者每日成功領取k碼特權次數的并發更改。

watch機制:對鍵值進行監控,當被其他用戶端改變時,
目前的用戶端的所有操作将會失敗,抛出錯誤資訊。
           

3.使用者并發更新同一k碼特權商品庫存、同一商品的具體某個item

上述問題,屬于對竟态資源的并發修改,在接口請求并發量不大、且讀多寫少的情況下,采用資料庫樂觀鎖來解決問題。
資料庫樂觀鎖實作方式:
在競态資源(商品)記錄上添加一列,update_version,表示更新次數。
資料庫樂觀鎖實作方式僞代碼:
for(;;){
    //擷取某k碼商品庫存,更新版本号 sql
    $getRewardStcokSql = 'select reward_stock,reward_update_version from fx_platform_reward_amount where reward_type_id = {$reward_type_id}';
    $getReardStockResult = $model->query($getRewardStcokSql);
    if(!$getReardStockResult ){
        die;
    } 
    $reward_stock = getReardStockResult['reward_stock'];
    $reward_update_version = getReardStockResult['reward_update_version'];
    //如果庫存量>0
    if($reward_stock>0){
      //更新k碼商品庫存,版本号需要進行對比,其實本質上是不再使用資料庫提供的排它鎖,而将排他控制的職責交給選擇某條需要更新記錄的過濾條件。
        $updateRewardStockSql = 'update fx_platform_reward_amount set reward_stock = reward_stock-1 and reward_update_version = reward_update_version + 1 where reward_type_id = {$reward_type_id} reward_update_version = {$reward_update_version} ';
        $updateRewardStockResult = $model->excute($updateRewardStockSql);
    }
    //并發更新失敗,表示在此使用者更新商品庫存之前已經有使用者更新成功,需要重新嘗試更新。
    if(!$updateRewardStockResult){
        continue;
    }
}
           

7.測試結果

7.1 并發測試,資料能保持一緻性
7.2 使用者免費領取k碼特權商品響應時間均值為 110ms 左右,
      使用者當日已領取過k碼特權獎品的接口響應時間40-55ms左右。