[原]記錄一個由于InnoDB MVCC導致的并發(fā)BUG

上周公司支付系統(tǒng)出現(xiàn)了故障,在故障處理后的善后排查中發(fā)現(xiàn)了某賬戶某段時間內(nèi)資金和流水對不上树绩,最終發(fā)現(xiàn)了一個并發(fā)BUG峦筒。由于該BUG具有一定普通適性,故整理成文到內(nèi)部知識庫锄列,簡書也順便update了图云。

數(shù)據(jù)定義

能夠說明問題的最簡數(shù)據(jù)如下。

CREATE TABLE `purse` (
  `id` int(11) NOT NULL AUTO_INCREMENT ,
  `money` DECIMAL(13,2) NOT NULL DEFAULT 0.0 COMMENT '賬戶金額',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='錢包' ;
CREATE TABLE `user_order` (
  `id` int(11) NOT NULL AUTO_INCREMENT ,
  `money` DECIMAL(10,2) NOT NULL DEFAULT 0.0 COMMENT '訂單金額',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8  COMMENT='訂單';
INSERT purse VALUE (1,1);
INSERT user_order VALUE (1,10);
INSERT user_order VALUE (2,20);

偽代碼

能構(gòu)成該BUG的最簡偽代碼如下

1.sql('start transaction');
2.orderMoney=sql('select money from user_order where id =:id ')
3.sql('select money from user_purse where id =:id for update')
4.nowMoney=sql('select * from orders where id =:order_id ')+orderMoney
5.sql('user_purse  set avail_money=:nowMoney where id =:id')
6.sql('commit')

故障原理

定義兩個客戶端/事務為A/B,其中一種能夠觸發(fā)BUG的執(zhí)行流程如下邻邮。

//事務隔離級別為默認的REPEATABLE READ
A1 sql('start transaction');
B1 sql('start transaction');
A2 orderMoney=sql('select money from user_order where id =1' )//10
B2 orderMoney=sql('select money from user_order where id =2 ')//20
B3.sql('select money from user_purse where id = 1 for update')//1
B4.nowMoney=sql('select money from user_purse where id =1 ' )+orderMoney//21
B5.sql('user_purse  set avail_money=:nowMoney where id =3')//purse=21
B6.sql('commit')//purse =21
A3.sql('select money from user_purse where id =:id for update')//purse=21
A4.nowMoney=sql('select money from user_purse where id =1 ')+orderMoney //問題觸發(fā)點 purse=1+10
A5.sql('user_purse  set avail_money=:nowMoney where id =1')//purse =11
A6.sql('commit')//purse =11

示例代碼中竣况,通過語句3的for update對賬戶表的某一條記錄來施加行鎖以達到并發(fā)控制。

語句3~6間的流程即語句4筒严,5都處于臨界區(qū)丹泉,案例中由于事務B率先拿到了行x鎖,所以事務A的語句3一直阻塞到事務B語句6執(zhí)行完成鸭蛙。

臨界區(qū)的處理本身沒有任何問題摹恨。問題在于開發(fā)者在這里沒有考慮MVCC和隔離級別,導致語句4在臨界區(qū)引入了快照讀娶视。

SELECT FOR UPDATESELECT LOCK IN SHARE MODE這兩個操作在Innodb中被成為一致性鎖定讀
這兩者的性質(zhì)是通過加鎖來控制并發(fā)訪問晒哄,在讀取到的是記錄的(已提交)實時值。

普通的SELECT稱為一致性非鎖定讀其特性是通過MVCC(多版本)控制的方式讀取數(shù)據(jù)肪获。SELECT并不會去等待鎖的釋放寝凌,而是直接獲取數(shù)據(jù)的一個快照副本,在RR的隔離級別下孝赫,這個快照即事務開始時的數(shù)據(jù)快照较木。

因此A4讀到了事務B修改前的數(shù)據(jù)。最終導致事務B在purse表的同一行記錄被覆蓋了青柄。

解決方案

這個場景的修復方式有3種:

  1. 將隔離級別降低為RC(Read committed):RC的快照讀獲取的是數(shù)據(jù)已提交的最后一個版本伐债,本案例中A4拿到的將會是B6提交后更新21预侯,但這種辦法本質(zhì)是為事務引入’不可重復讀‘。

2.將語句3和語句4合并泳赋,如果代碼或框架上下文不允許合并雌桑。可以將語句4改成SELECT FOR UPDATE/SELECT LOCK IN SHARE MODE祖今,避免快照讀校坑。

3.使用update set money+XX這種自帶X鎖的表達式語法,這種是最建議的解決方案千诬,除了避免了此處的快照讀耍目,還避免了應用層到mysql數(shù)據(jù)轉(zhuǎn)換的潛在坑坑以及避免了加鎖和事務處理不當?shù)姆N種低級問題。在應用層計算數(shù)據(jù)更新后的值再寫入數(shù)據(jù)庫徐绑,是一種非常不健壯的方案邪驮,特別是在PHP這種弱類型語言中,服務異常導致的null隨時會讓你把某些數(shù)據(jù)初始化傲茄。

尾注:上文提到的“事務開啟時的快照”是業(yè)內(nèi)包括《高性能Mysql》《Mysql技術(shù)內(nèi)幕》對RR下MVCC策略的一般描述毅访。嚴格來說并不完全準確。

all consistent reads within the same transaction read the snapshot established by the first such read in that transaction.

RR隔離級別下盘榨,實際上快照讀使用的是事務中第一條查詢執(zhí)行時的數(shù)據(jù)快照喻粹,所以如果想要復現(xiàn),該問題例子中看似無關(guān)的語句2不能刪除草巡。如果語句2不存在守呜,除非事務B的提交(B6)在在事務A的for update查詢(A3)之前,才能復現(xiàn)該問題山憨。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末查乒,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子郁竟,更是在濱河造成了極大的恐慌玛迄,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棚亩,死亡現(xiàn)場離奇詭異憔晒,居然都是意外死亡,警方通過查閱死者的電腦和手機蔑舞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嘹屯,“玉大人攻询,你說我怎么就攤上這事≈莸埽” “怎么了钧栖?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵低零,是天一觀的道長。 經(jīng)常有香客問我拯杠,道長掏婶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任潭陪,我火速辦了婚禮雄妥,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘依溯。我一直安慰自己老厌,他們只是感情好,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布黎炉。 她就那樣靜靜地躺著枝秤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪慷嗜。 梳的紋絲不亂的頭發(fā)上淀弹,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天,我揣著相機與錄音庆械,去河邊找鬼薇溃。 笑死,一個胖子當著我的面吹牛干奢,可吹牛的內(nèi)容都是我干的痊焊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼忿峻,長吁一口氣:“原來是場噩夢啊……” “哼薄啥!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起逛尚,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤垄惧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后绰寞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體到逊,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年滤钱,在試婚紗的時候發(fā)現(xiàn)自己被綠了觉壶。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡件缸,死狀恐怖铜靶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情他炊,我是刑警寧澤争剿,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布已艰,位于F島的核電站,受9級特大地震影響蚕苇,放射性物質(zhì)發(fā)生泄漏哩掺。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一涩笤、第九天 我趴在偏房一處隱蔽的房頂上張望嚼吞。 院中可真熱鬧,春花似錦辆它、人聲如沸誊薄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽呢蔫。三九已至,卻和暖如春飒筑,著一層夾襖步出監(jiān)牢的瞬間片吊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工协屡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留俏脊,地道東北人。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓肤晓,卻偏偏與公主長得像爷贫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子补憾,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359