新聞中心
繼續(xù)解答星球水友提問。

| 沈老師,我們有個業(yè)務(wù),同一個用戶在并發(fā)“查詢,邏輯計算,扣款”的情況下,余額可能出現(xiàn)不一致,請問有什么優(yōu)化方法么? |
扣款的業(yè)務(wù)場景是怎樣的?
用戶購買商品的過程中,要對余額進行查詢與修改,大致的業(yè)務(wù)流程如下:第一步,從數(shù)據(jù)庫查詢用戶現(xiàn)有余額:
- SELECT money FROM t_yue WHERE uid=$uid;
不妨設(shè)查詢出來的$old_money=100元。
第二步,業(yè)務(wù)層實施業(yè)務(wù)邏輯計算,比如:
- 先查詢購買商品的價格,例如是80元;
- 再查詢產(chǎn)品是否有活動,以及活動折扣,例如是9折;
- 比對余額是否足夠,足夠時才往下走;
- if($old_money> 80*0.9){
- $new_money=$old_money-80*0.9=28
- } else {
- return "Not enough minerals";
- }
第三步,將數(shù)據(jù)庫中的余額進行修改。
- UPDATE t_yue SET money=$new_money WHERE uid=$uid;
在并發(fā)量低的情況下,這個流程沒有任何問題,原有金額100元,購買了80元的九折商品(72元),剩余28元。
同一個用戶,并發(fā)扣款可能出現(xiàn)什么問題?
在分布式環(huán)境中,如果并發(fā)量很大,這種“查詢+修改”的業(yè)務(wù)有一定概率出現(xiàn)數(shù)據(jù)不一致。
極限情況下,可能出現(xiàn)這樣的異常流程:
步驟一,業(yè)務(wù)1和業(yè)務(wù)2并發(fā)查詢余額,是100元。
畫外音:這些并發(fā)查詢,是在不同的站點實例/服務(wù)實例上完成的,進程內(nèi)互斥鎖肯定解決不了。
步驟二,業(yè)務(wù)1和業(yè)務(wù)2并發(fā)進行邏輯計算,算出各自業(yè)務(wù)的余額,假設(shè)業(yè)務(wù)1算出的余額是28元,業(yè)務(wù)2算出的余額是38元。
步驟三,業(yè)務(wù)1對數(shù)據(jù)庫中的余額先進行修改,設(shè)置成28元。業(yè)務(wù)2對數(shù)據(jù)庫中的余額后進行修改,設(shè)置成38元。
此時異常出現(xiàn)了,原有金額100元,業(yè)務(wù)1扣除了72元,業(yè)務(wù)2扣除了62元,最后剩余38元。
畫外音:假設(shè)業(yè)務(wù)1先寫回余額,業(yè)務(wù)2再寫回余額。
常見的解決方案?
對于此案例,同一個用戶,并發(fā)扣款時,有小概率會出現(xiàn)異常,可以對每一個用戶進行分布式鎖互斥,例如:在redis/zk里搶到一個key才能繼續(xù)操作,否則禁止操作。
這種悲觀鎖方案確實可行,但要引入額外的組件(redis/zk),并且會降低吞吐量。
對于小概率的不一致,有沒有樂觀鎖的方案呢?
對并發(fā)扣款進行進一步的分析發(fā)現(xiàn):
(1) 業(yè)務(wù)1寫回時,舊余額100,這是一個初始狀態(tài);新余額28,這是一個結(jié)束狀態(tài)。理論上只有在舊余額為100時,新余額才應(yīng)該寫回成功。
而業(yè)務(wù)1并發(fā)寫回時,舊余額確實是100,理應(yīng)寫回成功。
(2) 業(yè)務(wù)2寫回時,舊余額100,這是一個初始狀態(tài);新余額28,這是一個結(jié)束狀態(tài)。理論上只有在舊余額為100時,新余額才應(yīng)該寫回成功。
可實際上,這個時候數(shù)據(jù)庫中的金額已經(jīng)變?yōu)?8了,所以業(yè)務(wù)2的并發(fā)寫回,不應(yīng)該成功。
如何低成本實施樂觀鎖?
在set寫回的時候,加上初始狀態(tài)的條件compare,只有初始狀態(tài)不變時,才允許set寫回成功,Compare And Set(CAS),是一種常見的降低讀寫鎖沖突,保證數(shù)據(jù)一致性的方法。
此時業(yè)務(wù)要怎么改?
使用CAS解決高并發(fā)時數(shù)據(jù)一致性問題,只需要在進行set操作時,compare初始值,如果初始值變換,不允許set成功。
具體到這個case,只需要將:
- UPDATE t_yue SET money=$new_money WHERE uid=$uid;
- 升級為:
- UPDATE t_yue SET money=$new_money WHERE uid=$uid AND money=$old_money;
即可。
并發(fā)操作發(fā)生時:業(yè)務(wù)1執(zhí)行:
- UPDATE t_yue SET money=28 WHERE uid=$uid AND money=100;
業(yè)務(wù)2執(zhí)行:
- UPDATE t_yue SET money=38 WHERE uid=$uid AND money=100;
這兩個操作同時進行時,只可能有一個執(zhí)行成功。
怎么判斷哪個并發(fā)執(zhí)行成功,哪個并發(fā)執(zhí)行失敗呢?
set操作,其實無所謂成功或者失敗,業(yè)務(wù)能通過affect rows來判斷:
- 寫回成功的,affect rows為1
- 寫回失敗的,affect rows為0
總結(jié)
高并發(fā)“查詢并修改”的場景,可以用CAS(Compare and Set)的方式解決數(shù)據(jù)一致性問題。對應(yīng)到業(yè)務(wù),即在set的時候,加上初始條件的比對即可。
優(yōu)化不難,只改了半行SQL,但確實能解決問題。
但希望大家有收獲,思路比結(jié)論重要。
【本文為專欄作者“58沈劍”原創(chuàng)稿件,轉(zhuǎn)載請聯(lián)系原作者】
名稱欄目:并發(fā)扣款,如何保證數(shù)據(jù)的一致性?
標題網(wǎng)址:http://m.5511xx.com/article/cccddeg.html


咨詢
建站咨詢
