并發(fā)扣款一致性,冪等性問(wèn)題,這個(gè)話題還沒(méi)聊完?。?!
《并發(fā)扣款,如何保證數(shù)據(jù)的一致性?》,分享了同一個(gè)用戶并發(fā)扣款時(shí),有一定概率出現(xiàn)數(shù)據(jù)不一致,可以使用CAS樂(lè)觀鎖的方式,在不降低吞吐量,并且只有少量修改的情況下,保證數(shù)據(jù)的一致性。
文章發(fā)布不到24小時(shí),就有近200的評(píng)論。
其中,問(wèn)的比較多的是ABA問(wèn)題,這個(gè)問(wèn)題已經(jīng)在《并發(fā)扣款一致性優(yōu)化,CAS下ABA問(wèn)題,這個(gè)話題還沒(méi)聊完!!!》中擴(kuò)展。 其次,問(wèn)的比較多的是作業(yè)題,為什么一定要用select&set的方式進(jìn)行余額寫(xiě)回:
- UPDATE t_yue SET money=$new_money WHERE uid=$uid AND money=$old_money;
為什么不能采用直接扣減的方法:
- UPDATE t_yue SET moneymoney=money-$diff WHERE uid=$uid;
很人說(shuō),在并發(fā)情況下,會(huì)將money扣成負(fù)數(shù)。 為了保證余額不被扣成負(fù)數(shù),再加一個(gè)where條件:
- UPDATE t_yue SET moneymoney=money-$diff WHERE uid=$uid AND money-$diff>0;
這樣是否可行?畫(huà)外音:額,撇開(kāi)業(yè)務(wù)不談,這個(gè)SQL用列做運(yùn)算,其實(shí)是不好的,建議使用:
- UPDATE t_yue SET moneymoney=money-$diff WHERE uid=$uid AND money>$diff;
很遺憾,仍然不行。原因在《并發(fā)扣款,如何保證數(shù)據(jù)的一致性?》一文里點(diǎn)贊最多的評(píng)論,不冪等。畫(huà)外音:說(shuō)明絕大部分同學(xué),能夠回答正確作業(yè)。 聊冪等性之前,先看另一個(gè)測(cè)試用例的case。 假設(shè)有一個(gè)服務(wù)接口,注冊(cè)新用戶:
- bool RegisterUser($uid, $name){
- //查看uid是否已經(jīng)存在
- select uid from t_user where uid=$uid;
- //不是新用戶,返回失敗
- if(rows>0)return false;
- else{
- //把新用戶插入用戶表
- insert into t_user values($uid, $name);
- //返回成功
- return true;
- }
- }
有一個(gè)測(cè)試工程師,對(duì)該接口寫(xiě)了一個(gè)測(cè)試用例:
- bool TestCase_RegisterUser(){
- //造一些假數(shù)據(jù)
- long uid=123;
- String name='shenjian';
- //調(diào)用被測(cè)試的接口
- bool result= RegisterUser(uid,name);
- //預(yù)期注冊(cè)成功,對(duì)結(jié)果進(jìn)行斷言判斷
- Assert(result,true);
- //返回測(cè)試結(jié)果
- return result;
- }
這是不是一個(gè)好的測(cè)試用例?這個(gè)用例存在什么問(wèn)題?
你會(huì)發(fā)現(xiàn),相同條件下,這個(gè)測(cè)試用例執(zhí)行兩次,得到的結(jié)果不一樣:
- 第一次執(zhí)行,第一次造數(shù)據(jù),調(diào)用接口,注冊(cè)成功;
- 第二次執(zhí)行,又造了一次相同的數(shù)據(jù),調(diào)用接口,注冊(cè)會(huì)失敗;這不是一個(gè)好的測(cè)試用例,多次執(zhí)行結(jié)果不同。
什么是冪等性?
相同條件下,執(zhí)行同一請(qǐng)求,得到的結(jié)果相同,才符合冪等性。
畫(huà)外音:Google一下,比我解釋得更好,但意思應(yīng)該說(shuō)清楚了。
如何將上面的測(cè)試用例改為符合“冪等性”的測(cè)試用例呢?
只需要加一行代碼:
- bool TestCase_RegisterUser(){
- //造一些假數(shù)據(jù)
- long uid=123;
- String name=’shenjian’;
- //先刪除這個(gè)偽造的用戶
- DeleteUser(uid);
- //調(diào)用被測(cè)試的接口
- bool result= RegisterUser(uid,name);
- //預(yù)期注冊(cè)成功,對(duì)結(jié)果進(jìn)行斷言判斷
- Assert(result,true);
- //返回測(cè)試結(jié)果
- return result;
- }
這樣,在相同條件下,不管這個(gè)用例執(zhí)行多少次,得到的測(cè)試結(jié)果都是相同的。 是不是對(duì)冪等性有點(diǎn)感覺(jué)了。 讀請(qǐng)求,一般是冪等的。
寫(xiě)請(qǐng)求,視情況而定:
- insert x,一般來(lái)說(shuō)不是冪等的,重復(fù)插入得到的結(jié)果不一定一樣
- delete x,一般來(lái)說(shuō)是冪等的,刪除多次得到的結(jié)果仍相同
- set a=x是冪等的
- set a=a-x不是冪等的
- …
因此,這么扣減余額:
- UPDATE t_yue SET money=$new_money WHERE uid=$uid AND money=$old_money;
是冪等操作。
要是這么扣減余額:
- UPDATE t_yue SET moneymoney=money-$diff WHERE uid=$uid AND money-$diff>0;
不是冪等操作。
聊到這里,或許有朋友要抬杠了,測(cè)試用例會(huì)重復(fù)執(zhí)行,扣款怎么會(huì)重復(fù)執(zhí)行呢?
重試。 重試,是異常處理里很常見(jiàn)的手段。
你在寫(xiě)業(yè)務(wù)的時(shí)候有沒(méi)有寫(xiě)過(guò)這樣的代碼:
- result = DoSomething();
- if(false==result || TIMEOUT){
- //錯(cuò)誤,或者超時(shí),重試一次
- result= DoSomething();
- }
- return result;
當(dāng)然,又會(huì)有朋友抬杠了,我從來(lái)不重試!!!
畫(huà)外音:額,這是合格,還是不合格呢?
你可以決定業(yè)務(wù)代碼怎么寫(xiě),你不能決定底層框架代碼怎么寫(xiě):
- 站點(diǎn)框架有沒(méi)有自動(dòng)重試?
- 服務(wù)框架有沒(méi)有自動(dòng)重試?
- 服務(wù)連接池,數(shù)據(jù)庫(kù)連接池有沒(méi)有自動(dòng)重試?
畫(huà)外音:
- 服務(wù)化分層的架構(gòu)中,建議只入口層重試,服務(wù)層不要重試,防止雪崩;
- dubbo底層,調(diào)用超時(shí)是默認(rèn)重試的,這個(gè)設(shè)計(jì)不好;
因此,在有重試的架構(gòu)體系里,冪等性是需要考慮的一個(gè)問(wèn)題。
現(xiàn)在該懂了,為啥扣款和充值業(yè)務(wù),一般使用:select&set,配合CAS方案
而不使用:set money-=X方案
畫(huà)外音:充了100電話費(fèi),怎么多了200塊?
知其然,知其所以然,希望大家有收獲。
【本文為51CTO專(zhuān)欄作者“58沈劍”原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)聯(lián)系原作者】