線上賬務系統餘額並發更新問題記錄
(點擊
上方公眾號
,可快速關注)
來源:hebaodan ,
my.oschina.net/hebaodan/blog/917141
線上賬務系統餘額並發更新問題記錄
某電商平台,某天線上用戶報bug說賬戶餘額信息與交易流水對不上。可以認為是資料庫並發更新問題,由此定位出具體原因,並給出解決方案。
問題現象
場景描述
線上賬務系統,在定時結算給賣家錢時,且高並發量的情況下,出現提現x元(假設當前用戶餘額為x元)餘額為0後,再轉入該賬戶一筆錢(假設為y元),結果賬戶餘額變為了x+y 元,導致用戶餘額錯誤。 ps:賬戶餘額的變更都是在事務中update的
環境說明
mysql5.7 + innodb,事務隔離級別是REPEATABLE-READ
場景模擬
我們簡化下線上的數據結構,進行場景模擬。 數據表如下:
『賬戶主表』
CREATE TABLE user (
uid int(11) NOT NULL COMMENT "類型id+自增序列",
name varchar(32) DEFAULT NULL,
PRIMARY KEY (uid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT="賬戶主表"
『賬戶餘額明細表』
CREATE TABLE user_account (
uid int(11) NOT NULL,
amount decimal(19,4) DEFAULT 0 COMMENT "賬戶餘額",
PRIMARY KEY (uid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT="賬戶餘額明細表"
賬戶類型配置
CREATE TABLE user_conf (
type_id int(11) NOT NULL, description varchar(32) DEFAULT NULL COMMENT "類型描述", PRIMARY KEY (type_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT="賬戶類型配置"
具體數據為:
select * from user;
+-------+------+ | uid | name | +-------+------+
| 10001 | a |
| 10002 | b |
select * from user_account;
+-------+----------+ | uid | amount | +-------+----------+
| 10001 | 10.0000 |
| 10002 | 108.9900 |
select * from user_conf;
+---------+--------------+ | type_id | description | +---------+--------------+
| 100 | 外部賬戶 |
| 200 | 內部賬戶 |
模擬提現(即餘額減)和入賬(即餘額加)並發操作的事務如下:
問題出現了,後面再查詢該用戶餘額為30元,即用戶提現的10元未反映在餘額中
原因定位
熟悉mysql的同學或許已經知道問題是由REPEATABLE-READ隔離級別下快照讀導致。
具體解釋:
RR級別下,第一次讀操作會生成快照,對於可見性來說,只有當第一次讀之前其他事務提交的修改和自己的修改可見,其他的均不可見。
官網文檔:https://dev.mysql.com/doc/refman/5.7/en/glossary.html snapshot A representation of data at a particular time, which remains the same even as changes are committed by other transactions.
With REPEATABLE READ isolation level, the snapshot is based on the time when the first read operation is performed.
可見性原理
可參考文章:http://hedengcheng.com/?p=148
回到上述模擬場景中,session2在sql語句select description from user_conf where type_id = 100; 時已生成快照,雖然session1提交了,但仍然不可見,導致並發更新問題。
另外,開啟事務後,SELECT … FOR UPDATE 是不會生成快照的,大家可自行實驗
解決方案
方案一
將REPEATABLE-READ隔離級別改為READ-COMMITTED,這樣即能看到最新提交的數據。
方案二
在讀』賬戶餘額明細表』user_account 的時候加 for update,這樣會 1.強制讀該行記錄的最新版本數據,2.且若其他事務未commit,本事務將阻塞,保證串列更新
方案三
延時生成快照。開啟事務後,首先就通過user表做互斥,直接for update加鎖,針對多個事務並發更新即變為串列。
附:定位過程
針對上報bug用戶,查詢其交易流水明細與餘額變更明細,確認賬務存在問題
查詢賬務系統近幾天是否有上線變更,檢查無
拉取賬務資料庫mysql general log,找到並發更新的兩個事務session
查詢資料庫設置的隔離級別為RR,查詢應用資料庫連接池配置即session的隔離級別未配置,採用資料庫配置
確認由RR級別導致(當然也可以認為是代碼問題導致)
確認是一個月前賬務系統分庫分表上線,改用其他連接池且未設置session隔離級別。而之前是有配置session的隔離級別為READ-COMMITTED。
延伸思考
mysql RR級別適用的業務場景是什麼,應該怎麼選擇? 有興趣或有見解的同學可以留言回復或私信~~
參考
http://blog.csdn.net/chen77716/article/details/6742128#comments
http://hedengcheng.com/?p=148
https://liuzhengyang.github.io/2017/04/18/innodb-mvcc/
看完本文有收穫?請轉發分享給更多人
關注「ImportNew」,提升Java技能
TAG:ImportNew |