1. 程式人生 > >上賬務系統余額並發更新問題記錄

上賬務系統余額並發更新問題記錄

primary -- 並發 數據庫 .com sel 生成 sam nap

某電商平臺,某天線上用戶報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 | 內部賬戶 |

模擬提現(即余額減)和入賬(即余額加)並發操作的事務如下:

session1-提現10元session2-入賬20元begin;select description from user_conf where type_id = 100;select from user where uid = 10001 for update; // user表用來做互斥select amount from user_account where uid = 10001; // 10.00begin;select description from user_conf where type_id = 100;select from user where uid = 10001 for update; // wating//watingupdate user_account set amount = 0.00 where uid = 10001;commit;拿到鎖select amount from user_account where uid = 10001; //10.00入賬20元,代碼中計算後應該為30元update user_account set amount = 30.00 where uid = 10001;commit;

問題出現了,後面再查詢該用戶余額為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.

可見性原理

回到上述模擬場景中,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級別適用的業務場景是什麽,應該怎麽選擇? 有興趣或有見解的同學可以留言回復或私信~~

上賬務系統余額並發更新問題記錄