1.【背景】
斐訊路由App 需要新增k碼特權(quán)模塊沐绒。
2.【需求】
1.已通過k碼激活狀態(tài)驗(yàn)證的用戶可免費(fèi)領(lǐng)取k碼特權(quán)商品
2.每個(gè)用戶每天只能領(lǐng)取一張k碼特權(quán)獎品
3.【應(yīng)用場景及難點(diǎn)分析】
1.接口數(shù)據(jù)安全性要求:
1.1 當(dāng)某k碼特權(quán)商品數(shù)據(jù)量為1,且高并發(fā)情況下储耐,
1.2 如何防止超賣(即多個(gè)用戶都搶到了剩余的一個(gè)商品)
2.接口性能要求:
斐訊路由App 現(xiàn)用戶量為300w+,日活4w+郑气,2/8原則分析(**指80%的業(yè)務(wù)量在20%的時(shí)間里完成**)怠堪。
經(jīng)驗(yàn)可知用戶使用斐訊路由App 的持續(xù)時(shí)間為12小時(shí),
所以2/8分析后雷逆,80%的日活在20%的時(shí)間內(nèi)完成弦讽。
即32000人免費(fèi)領(lǐng)取k碼特權(quán)商品要在2.4小時(shí)內(nèi)完成,
換算成每秒 完成請求數(shù)即 QPS = 3.7/s 关面。
即每個(gè)接口響應(yīng)請求時(shí)間至少要在 270ms 以內(nèi)坦袍。才算是高性能十厢。
4.問題分析:
1.讀多寫少
每個(gè)用戶每日只能領(lǐng)取一個(gè)k碼特權(quán)商品。即1個(gè)用戶加入請求免費(fèi)領(lǐng)取k碼特權(quán)接口多次捂齐,在k碼商品庫存量充足的情況下蛮放,只能領(lǐng)取到1個(gè)商品,其余請求都應(yīng)該返回“對不起奠宜,您今日已領(lǐng)取k碼特權(quán)商品”包颁。從這個(gè)方面來定義,其屬于讀多寫少的問題压真。
2.并發(fā)量低
以斐訊路由現(xiàn)在日活情況為4w+的數(shù)據(jù)量來估算娩嚼、接口并發(fā)能力 QPS = 3.7/s ,
屬于低并發(fā)滴肿,但在k碼特權(quán)模塊優(yōu)化程度達(dá)到一定量時(shí)岳悟,并發(fā)量是否會上升有待考察。
但總體來說屬于并發(fā)量不高的場景泼差。
也就是說k碼特權(quán)問題經(jīng)過模型抽象贵少,已經(jīng)變成了讀多寫少、并發(fā)量不大堆缘,但要保證性能滔灶,和數(shù)據(jù)安全性一致性的問題。
對于這類問題吼肥,樂觀鎖思想可以作為解決這類問題的指導(dǎo)思想录平。
5.樂觀鎖思想
網(wǎng)上文章對樂觀鎖理解的誤區(qū):
1.樂觀鎖是一種思想,并不是一種具體的技術(shù)實(shí)現(xiàn)缀皱。
2.樂觀鎖類似于CAS無鎖編程技術(shù)(其實(shí)也加鎖斗这,只不過在cpu層面)
即當(dāng)多個(gè)線程同時(shí)并發(fā)更新統(tǒng)一個(gè)變量,
采用先select再update的方式啤斗,select出當(dāng)前變量a的副本值b涝影,然后用新值c去更新,
更新時(shí)需要拿select 出來的變量值a的副本值b與當(dāng)前非副本變量a的值做對比争占,
若暫存副本值b與當(dāng)前變量a非副本值相同燃逻,則正常更新,
如果不同臂痕,則認(rèn)為在當(dāng)前線程更新之前已經(jīng)有一個(gè)值將a變量更新伯襟,
則更新失敗,在并發(fā)情況不大的情況下握童,
采用循環(huán)的方式去更新姆怪,總能更新成功,且循環(huán)更新次數(shù)不會太多。因此CAS也叫自旋鎖稽揭。
6.k碼特權(quán)-免費(fèi)領(lǐng)取解決方案
1.用戶每日成功領(lǐng)取k碼特權(quán)商品次數(shù)的限制
采用redis 數(shù)據(jù)結(jié)構(gòu) String俺附,記錄用戶每日免費(fèi)領(lǐng)取成功次數(shù)。并且可以輕松使用redis 緩存的過期 (expire) 機(jī)制做每日領(lǐng)次數(shù)的控制)溪掀,用戶每日成功領(lǐng)取k碼特權(quán)的次數(shù)次日凌晨清空事镣。
為什么不使用數(shù)據(jù)庫來進(jìn)行用戶成功領(lǐng)取k碼特權(quán)商品次數(shù)的控制。當(dāng)然建立好索引此問題也可以完美解決揪胃。
使用redis進(jìn)行用戶成功領(lǐng)取k碼特權(quán)商品次數(shù)的控制原因有兩個(gè):
1.因?yàn)閞edis 純粹的查詢快璃哟,減輕數(shù)據(jù)庫壓力!不用每次都通過數(shù)據(jù)庫二次索引喊递,從磁盤找到目標(biāo)記錄并讀入到內(nèi)存随闪。**
2.線上配置的redis使用量10%都不到,為了更好的利用硬件資源骚勘。**
2.用戶每日成功領(lǐng)取k碼特權(quán)次數(shù)的并發(fā)更改
從接口安全性考慮铐伴,若有用戶惡意領(lǐng)取、那么有可能產(chǎn)生一個(gè)用戶在一天之內(nèi)領(lǐng)取了多個(gè)k碼特權(quán)獎品俏讹,這個(gè)是業(yè)務(wù)需求所不允許的盛杰。
這里我們使用到了redis 提供的 事務(wù)(multi)與watch(樂觀鎖實(shí)現(xiàn)) 機(jī)制來控制 用戶每日成功領(lǐng)取k碼特權(quán)次數(shù)的并發(fā)更改。
watch機(jī)制:對鍵值進(jìn)行監(jiān)控藐石,當(dāng)被其他客戶端改變時(shí),
當(dāng)前的客戶端的所有操作將會失敗定拟,拋出錯(cuò)誤信息于微。
3.用戶并發(fā)更新同一k碼特權(quán)商品庫存、同一商品的具體某個(gè)item
上述問題青自,屬于對竟態(tài)資源的并發(fā)修改株依,在接口請求并發(fā)量不大、且讀多寫少的情況下延窜,采用數(shù)據(jù)庫樂觀鎖來解決問題恋腕。
數(shù)據(jù)庫樂觀鎖實(shí)現(xiàn)方式:
在競態(tài)資源(商品)記錄上添加一列,update_version逆瑞,表示更新次數(shù)荠藤。
數(shù)據(jù)庫樂觀鎖實(shí)現(xiàn)方式偽代碼:
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碼商品庫存获高,版本號需要進(jìn)行對比哈肖,其實(shí)本質(zhì)上是不再使用數(shù)據(jù)庫提供的排它鎖,而將排他控制的職責(zé)交給選擇某條需要更新記錄的過濾條件念秧。
$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);
}
//并發(fā)更新失敗淤井,表示在此用戶更新商品庫存之前已經(jīng)有用戶更新成功,需要重新嘗試更新。
if(!$updateRewardStockResult){
continue;
}
}
7.測試結(jié)果
7.1 并發(fā)測試币狠,數(shù)據(jù)能保持一致性
7.2 用戶免費(fèi)領(lǐng)取k碼特權(quán)商品響應(yīng)時(shí)間均值為 110ms 左右游两,
用戶當(dāng)日已領(lǐng)取過k碼特權(quán)獎品的接口響應(yīng)時(shí)間40-55ms左右。