- Part One
- Part Two
Part One
我們假想一個場景,在并發場景下,假如要對賬戶余額(tb_balance.balance)進行增加和減少余額,要怎么設計才能保證數據不出錯呢?
下面是我的一些設想(假設余額更新20或減少20):
直接sql更新
直接balance自增20,怎么并發都余額都不會錯了是不是~,但這里會有問題,1.假如是減余額的話,要注意余額不能小于0 2.假如后續需要用這個余額結果再進行一些業務操作的話,是取不到這個余額的sql
updatetb_balancesetbalance=balance+20wherebalance=#{old_bal}anduser_id=#{user_id}
CAS
經典的compare and swap,就是入庫時去對比舊值是否與現數據庫中的值相等,若相等則更新否則不更新,但是會有風險,就是經典的aba問題。假如更新返回的影響數為0的話,說明余額已經發生了變化,所以需要拋出異常并進行重試。(這里需要寫一些補償的業務邏輯去處理余額更新失敗的問題)
/**
*用戶余額增加20
*newBal為新值
*oldBal為庫中查出的值
**/
oldBal=balService.getBal(userId);
newBal=oldBal+20;
intcount=balService.updateBal(newBal,oldBal,userId);
Assert.businessInvalid(count==0,"updateerror");
//todosomebuisness
updatetb_balancesetbalance=#{new_bal}wherebalance=#{old_bal}anduser_id=#{user_id}
樂觀鎖
樂觀鎖其實就是使用version去進行版本控制,在更新時判斷是否更新數據的版本與現庫內版本是一致的,若一致則更改并上升版本號,否則認為在此期間有其它線程進行數據更新。假如更新失敗的話,同樣進行重試處理。這樣的好處就是可以避免aba問題,同時也可以使用增加后的余額進行后續的操作。目前已知有些公司就是這么操作的
try{
//dosomebuisness
intcount=balService.updateBal(newBal,old_version,new_version,userId);
Assert.businessInvalid(count==0,"updateerror");
//dosomebuisness
}catch(){
//iffailtodosomethingtryagain
}
updatetb_balancesetbalance=#{balance}andbalance=#{new_version}whereversion=#{old_version}anduser_id=#{user_id}
--ifaffectcount>1success
--ifaffectcount=0retry
redission分布式鎖
像集群的項目,我們經常會使用分布式鎖去保證冪等性,如果只有單一接口會去操作賬戶余額那使用分布式鎖沒有問題,只要在該接口加上鎖,保證同一時間只有單一線程進行該業務操作即可;但往往實際業務場景并不會那么簡單,比如一個商城,可能會有幾十上百個入口可以對余額進行變更,比如定時扣費、訂單支付、替他人代付等等;那其實可以通過面向對象的思想,將余額的操作進行抽象,抽象出一個余額類,將該類的方法加上鎖,然后所有的業務都強制通過該類進行余額變更的操作,即可保證操作的可靠性。
所以也就是說,訂單系統、服務管理中心等等服務都不應該能直接操作到余額數據,還是得抽出一個類型財務中臺的東東去統一處理余額,然后財務中臺中又維護這么一個余額類去保證更新的可靠性? 這其實也就是軟件工程中的單一入口 思想吧
基于 Spring Boot + MyBatis Plus + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
- 項目地址:https://github.com/YunaiV/ruoyi-vue-pro
- 視頻教程:https://doc.iocoder.cn/video/
Part Two
業務背景:現在實際開發過程中會有這樣一種場景,訂單表a作為訂單業務核心表,而會有a1,a2,a3...an個業務會去更新訂單表a的狀態,而同時a.status(訂單狀態)也是作為b,c,d...業務操作的核心判斷依據(也就是在不同的訂單狀態下可能做的操作是截然相反的)
在代碼層面上,我們一般的寫法是直接用注解開個事務,以保證操作的原子性。這種寫法目前讓我們的業務還是平穩運行的~
//@apiLock是redis鎖
@ApiLock
@Transactional
publicvoiddosomething(){
}
那么假設用戶量激增,并發量暴漲,那么會出現什么情況呢?
搞事情的時間來了,事務只能保證單個操作的事務性,假如并發量高的時候,可能就會出現(假設b業務的前提是a訂單再待接單[wait_acceipt]的狀態):
b業務查詢時,a.status=wait_acceipt,然后b業務開始處理;而此時,a1業務對a訂單進行操作,并且先于b業務處理完,這時a訂單走到了待服務(wait_service),而b還在進行業務處理;然后b業務處理完成,并進行提交。
那么就會產生很多異常的數據了,而且影響范圍會很大
解決方案&一些思考
我們可以關注到,這個場景的問題點其實并不在數據的插入與更新,而是在讀取,在業務處理過程怎么在確定訂單狀態沒有發生改變
使用redis鎖,在讀取訂單時候對訂單進行上鎖,業務結束之后再釋放
單一入口 :將訂單類的操作抽象成一個order類,然后在這個order類中去進行查詢 操作,或者在不同服務中用一個redis-key也是OK的
publicclassOrderService{
lock()
select()
unlock()
}
publicvoiddosomeB(){
try{
orderService.lock(serviceOrderId);
//todo
}finally(){
orderService.unlock(serviceOrderId);
}
}
使用mysql共享鎖(讀鎖),在數據庫層面進行阻塞,更加精準,并且不使用單一入口也是可以的,但是可能存在索引失效鎖表的風險(核心表被鎖就炸了)
publicclassOrderService{
/**
*讀取并且上鎖
*/
selectAndLock()
/**
*讀取
*/
select()
}
--讀鎖會阻塞寫(X),但是不會堵塞讀(S),在事務提交后,讀鎖會自動釋放
selectxxxfromorderwhereservice_order_id=xxxlockinsharemode;
總體來說 ,思想就是對正在操作的數據進行加讀鎖,阻塞其它的線程。當然這種方案也是雙刃劍,畢竟會減少吞吐量,還是應該進行業務梳理,確定加鎖的必要性,避免過分設計~像現在的系統,我就沒去動它,hh~
-
數據庫
+關注
關注
7文章
3794瀏覽量
64362 -
Version
+關注
關注
0文章
32瀏覽量
7551
原文標題:并發場景下如何保證數據操作的準確性?
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論