場景介紹晒喷,用戶有100塊錢孝偎,同時給100個人轉(zhuǎn)賬,每個收款用戶1快錢凉敲。在高并發(fā)下衣盾,付款方扣減金額時寺旺,客戶資金安全不可控制,且容易發(fā)生死鎖势决。
數(shù)據(jù)表:賬戶表account阻塑、流水表txnlog
create table account (
'custname' varchar(32) not null comment '姓名',
'acct' varchar(32) not null comment '賬號',
'amt' bigdecimal not null comment '賬戶金額',
'identify' varchar(32) not null comment '證件號',
'identifynum' varchar(32) not null comment '證件類型'
primary key 'acct',
unique index idx('identify','identifynum'),
unique index idx_name('custname')
)
comment='賬戶表'
collate='uft8_bin'
engine='InnoDB'
)
create table txnlog (
'id' int(11) not null comment 'id',
'custname' varchar(32) not null comment '姓名',
'acct' varchar(32) not null comment '賬號',
'amt' bigdecimal not null comment '賬戶金額',
'skacct' varchar(32) not null comment '收款賬號',
'skcustname' varchar(32) not null comment '收款姓名'
primary key 'id',
)
comment='流水表'
collate='uft8_bin'
engine='InnoDB'
高并發(fā)資金安全問題分析
兩個轉(zhuǎn)賬請求同時到達(dá)并處理,對應(yīng)A果复、B兩個事務(wù)陈莽。存在問題:臟讀、可重復(fù)讀据悔、不可重復(fù)讀(官方定義為幻像/幻讀問題)传透、丟失更新。
事務(wù) | 動作 |
---|---|
事務(wù)A |
1极颓、查詢賬戶表賬戶金額朱盐; 2、扣減賬戶金額更新賬戶表菠隆; 3兵琳、生成轉(zhuǎn)出流水; 4骇径、查詢賬戶表賬戶金額
|
事務(wù)B |
1躯肌、查詢賬戶表賬戶金額; 2破衔、扣減賬戶金額更新賬戶表清女; 3、生成轉(zhuǎn)出流水晰筛; 4嫡丙、查詢賬戶表賬戶金額
|
A、B事務(wù)的處理順序如下 :
步驟 | A | B |
---|---|---|
1 | 查詢賬戶金額 | |
2 | 查詢賬戶金額 | |
3 | 扣減賬戶金額更新賬戶表 | |
4 | 生成轉(zhuǎn)出流水 | |
5 | 查詢賬戶金額 | |
6 | 扣減賬戶金額更新賬戶表 | |
7 | 生成轉(zhuǎn)出流水 | |
8 | 查詢賬戶金額 |
innodb存儲引擎默認(rèn)隔離級別repeatable read(可重復(fù)讀)读第,解決了臟讀問題曙博,保證了在同一事務(wù)中,多次讀取同樣事務(wù)的結(jié)果是一致的怜瞒。 但是理論上父泳,可重復(fù)讀隔離級別還是無法解決另一個幻讀(phantom read)問題。所謂幻讀吴汪,指當(dāng)前事務(wù)在讀取某個范圍內(nèi)的記錄時惠窄,另一個事務(wù)又在該范圍內(nèi)插入了新的記錄。當(dāng)之前的事務(wù)又在該范圍內(nèi)插入了新的記錄漾橙,會產(chǎn)生幻行杆融。更通俗點(diǎn),phantom problem是指在同一事務(wù)下近刘,連續(xù)執(zhí)行兩次同樣的sql語句可能導(dǎo)致不同的結(jié)果擒贸,第二次的sql語句可能會返回之前不存在的行。A中若增加統(tǒng)計(jì)流水總數(shù) select count(*) from txnLog;觉渴,可模擬出幻讀問題
介劫。 為解決幻讀問題,innodb存儲引擎引入了多版本并發(fā)控制(MVCC)解決幻讀問題案淋。
附:mvcc的實(shí)現(xiàn)座韵,是通過保存數(shù)據(jù)在某個時間點(diǎn)的快照來實(shí)現(xiàn)的,也就是說踢京,不管需要執(zhí)行多長時間誉碴,每個事務(wù)內(nèi)看到的數(shù)據(jù)都是一致的。根據(jù)事務(wù)開始的時間不同瓣距,每個事務(wù)對同一張表黔帕,同一時刻看到的數(shù)據(jù)可能是不一樣的。 innodb的MVCC蹈丸,是通過在每行記錄后面保存兩個隱藏的列來實(shí)現(xiàn)的成黄。這兩個列,一個保存了行的創(chuàng)建時間逻杖,一個保存了行的過期時間(或刪除時間)奋岁,當(dāng)然存儲的并不是實(shí)際的時間值,而是系統(tǒng)版本號荸百,每開始一個新的事務(wù)闻伶,系統(tǒng)版本號都會自動遞增。事務(wù)開始時刻的系統(tǒng)版本號會作為事務(wù)的版本號够话,用來和查詢到的每行記錄的版本號進(jìn)行比較蓝翰。
innodb存儲引擎默認(rèn)隔離級別repeatable read (可重復(fù)讀),mvcc的具體操作:
select更鲁, innodb會個根據(jù)以下兩個條件檢查每行記錄霎箍。 1、innodb只查找版本早于當(dāng)前事務(wù)版本的數(shù)據(jù)行(行的系統(tǒng)版本號小于或等于事務(wù)的系統(tǒng)版本號)澡为,這樣可以確保事務(wù)讀取的行漂坏,要么是在事務(wù)開始前已經(jīng)存在,要么是事務(wù)自身插入或者修改過的媒至。 2顶别、行的刪除版本要么未定義(未執(zhí)行過刪除操作),要么大于當(dāng)前事務(wù)版本號拒啰,可確保事務(wù)讀取到的行驯绎,在事務(wù)開始之前未被刪除。
insert , innodb為新插入的每一行保存當(dāng)前系統(tǒng)版本號作為行版本號
delete谋旦, innodb為刪除的每一行保存當(dāng)前系統(tǒng)版本號作為行刪除標(biāo)示
update剩失, innodb為插入一行新數(shù)據(jù)屈尼,保存當(dāng)前系統(tǒng)版本號作為行版本號,同時保存當(dāng)前系統(tǒng)版本號到原來的行作為行刪除標(biāo)示拴孤。(更新一條記錄脾歧,分為兩步,新增一條記錄演熟,同時更新原記錄的刪除標(biāo)示)
在這個場景中鞭执,由于mvcc的作用,A無法看到B的修改芒粹,A中扣減賬戶金額是在A開始的快照上進(jìn)行的兄纺。若A修改成功,會導(dǎo)致B對賬戶表的修改丟失化漆。
解決方案:
- 方案一估脆,修改隔離級別為read committed(提交讀),一個事務(wù)開始時座云,只能看見已經(jīng)提交的事務(wù)所做的修改旁蔼。即A事務(wù)能看到B事務(wù)的修改。
生產(chǎn)環(huán)境不允許修改隔離級別疙教,本方案不可用
- 方案二棺聊,接受innodb默認(rèn)隔離級別repeatable read(可重復(fù)讀),
因innodb存儲引擎采用了next-key locking的算法避免phantom problem贞谓,如select * from t where a>2 for update限佩,其鎖住的是(2,+無窮)這個范圍加了X鎖裸弦,任何對這個范圍的插入都是不允許的祟同,從而避免phantom problem
。 對于賬戶表理疙、流水表在一個事務(wù)中的操作晕城。第一步對賬戶表加X鎖(select * from account where 賬號='123456' for update),后續(xù)再進(jìn)行賬戶修改窖贤、流水修改砖顷。 使得對同一賬戶的操作,其他事務(wù)需要等待X鎖赃梧。此外滤蝠,對于不可變值的查詢,可以提到事務(wù)開始前進(jìn)行授嘀。
并發(fā)下innodb主鍵索引與非主鍵索引同時使用死鎖問題分析
情景一物咳、A事務(wù),主鍵索引取的X鎖蹄皱,使用非主鍵索引更新览闰。B事務(wù)芯肤,使用非主鍵索引更新,如下
步驟 | A | B |
---|---|---|
1 |
主鍵索引 set autocommit=false; begin; select * from account where acct='123456' for update;
|
|
2 |
非主鍵索引 set autocommit=false; begin; update account set amt=amt+1 where identify='456789' and identifynum='01';
|
|
3 |
非主鍵索引 update account set amt=amt+1 where identify='456789' and identifynum='01' ;
|
|
4 |
mysql>update account set amt=amt+1 where identify='456789' and identifynum='01' ; ERROR 1213 (40001):Deadlock found when trying to get lock; try restarting transaction
|
執(zhí)行到A3压鉴,B4會話報(bào)錯死鎖纷妆。
- 之所以會造成B死鎖,在innodb存儲引擎下晴弃,行級鎖并不是直接鎖記錄,而是鎖索引逊拍。
- A1 的sql語句用的主鍵作為條件上鞠,鎖的是主鍵索引,A事務(wù)占用了主鍵芯丧。
- B2的sql語句芍阎,用的是非主鍵作為條件,mysql會先鎖非主鍵索引缨恒,再鎖主鍵索引谴咸,此時B事務(wù)占用了非主鍵索引,等待主鍵索引骗露。
- A3的sql語句岭佳,使用了非主鍵索引進(jìn)行update操作,會等待B的非主鍵索引釋放萧锉。A/B兩個事務(wù)間形成死鎖珊随。
- innodb通過死鎖檢測機(jī)制wait-for graph 等待圖, 檢測到死鎖回路柿隙,選擇回滾undo量最小的事務(wù)B叶洞,對其進(jìn)行回滾,讓A繼續(xù)執(zhí)行禀崖。
解決方案:
- 1衩辟、在所有賬戶表修改操作開始前,使用主鍵索引對賬戶表加鎖
select * from account where acct='123456' for update
波附。 再進(jìn)行賬戶修改艺晴、流水操作、其他掸屡。 - 2财饥、對賬戶表的查詢和修改,統(tǒng)一使用主鍵索引或非主鍵索引折晦。避免主鍵索引與非主鍵索引共用時造成的死鎖钥星。
附:測試代碼中死鎖的發(fā)生,1满着、A會話為eclipse編譯器執(zhí)行谦炒,代碼 select * from account where acct='123456' for update 下一行打斷點(diǎn)贯莺,執(zhí)行到斷點(diǎn)出。 2宁改、打開mysql的會話窗口缕探,執(zhí)行B2。 3还蹲、eclipse繼續(xù)往下走爹耗,A3執(zhí)行后,查看B會話被中斷谜喊,能看到死鎖提示潭兽。
死鎖是指兩個或兩個以上的事務(wù)在執(zhí)行過程中,因爭奪鎖資源而造成的一種互相等待的現(xiàn)象斗遏。解決死鎖問題最簡單的方法是超時山卦,即兩個事務(wù)互相等待時,當(dāng)一個等待時間超過設(shè)置的某一閾值诵次,其中一個事務(wù)進(jìn)行回滾账蓉,另一個等待的事務(wù)就能繼續(xù)進(jìn)行。但是從上述案例中逾一,并不是等待超時铸本,而是死鎖后直接觸發(fā)了事務(wù)回滾,顯然不是等待超時的解決方案遵堵。
innodb存儲引擎采用wait-for graph(等待圖)
的方式來進(jìn)行死鎖檢測归敬,較之超時的方案,這是一種更為主動的死鎖檢測方式鄙早。在每個事務(wù)請求鎖并發(fā)生等待時都會判斷是否存在回路汪茧,若存在則有死鎖,通常來說innodb存儲引擎會選擇回滾undo量最小的事務(wù)限番。
情景二舱污、模擬兩個非主鍵索引 unique index idx('identify','identifynum')、unique index idx_name('custname') 混用發(fā)生死鎖弥虐,
步驟 | A | B |
---|---|---|
1 |
主鍵索引 set autocommit=false; begin; update account set amt=amt+1 where custname=‘張三’;
|
|
2 |
非主鍵索引 identify + identifynum set autocommit=false; begin; update account set amt=amt+1 where identify ='45678' and identifynum='01';
|
|
3 |
非主鍵索引 custname update account set amt=amt+1 where identify ='45678' and identifynum='01';
|
|
4 |
死鎖中斷 mysql>update account set amt=amt+1 where identify='456789' and identifynum='01' ; ERROR 1213 (40001):Deadlock found when trying to get lock; try restarting transaction
|
死鎖發(fā)生過程分析
- A1獲取了非主鍵索引custname扩灯,然后獲取了主鍵索引acct
- B2獲取了非主鍵索引identify + identifynum ,等待主鍵索引acct
- A3需要獲取非主鍵索引identify + identifynum霜瘪,但該索引已被B獲取珠插,A3等待非主鍵索引identify + identifynum的釋放,死鎖發(fā)生颖对。
- innodb的死鎖檢測機(jī)制wait-for graph 等待圖捻撑,檢測到鎖等待回路,判斷死鎖發(fā)生,選擇回滾undo量最小的事務(wù)顾患。innodb判斷B為undo最小的事務(wù)番捂,中斷并回滾B事務(wù)。A事務(wù)繼續(xù)往下執(zhí)行江解。
情景三设预、模擬A、B會話均為相同主鍵索引
步驟 | A | B |
---|---|---|
1 |
主鍵索引 set autocommit=false; begin; select * from account where acct='123456' for update;
|
|
2 |
主鍵索引 set autocommit=false; begin; update account set amt=amt+1 where acct='123456';
|
|
3 | 停下來不繼續(xù)往下走 | |
4 |
等待超時 mysql>update account set amt=amt+1 where acct='123456'; ERROR 1205 (HY000):Lock wait timeout exceeded; try restarting transaction
|
A3停下來犁河,不往下走鳖枕,則B4會提示等待鎖超時,會話中斷桨螺。從A宾符、B會話都使用主鍵索引可知,會話只會等待鎖超時彭谁,不會立即中斷事務(wù)。因此在編碼中允扇,盡可能避免在對同一條記錄操作的事務(wù)中缠局,混用主鍵索引和非主鍵索引。