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左右。