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