值班问题:insert语句插入了两条数据?
`c1` int(10) unsigned NOT NULL AUTO_INCREMENT,
`c2` int(11) DEFAULT NULL,
`c3` int(11) DEFAULT '0',
`c4` int(11) DEFAULT NULL,
PRIMARY KEY (`c1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 在使用如下的語法時(shí),怎么會(huì)插入兩條?c2 = 1007 and c3 = 1的記錄? insert into aa(c2,c4) select 1007, 8 from dual where not exists(select * from aa where c2 = 1007 and c3 = 1);
毫無疑問,先排查一下用戶實(shí)例的binlog,發(fā)現(xiàn)確實(shí)一前一后有兩條插入語句。
那會(huì)不會(huì)是MySQL的bug呢?即使是bug,也得要先復(fù)現(xiàn)出來吧。
?
如何復(fù)現(xiàn)呢?
session 1:
begin?work;
insert into aa(c2,c4) select 1007, 8 from dual where not exists(select * from aa where c2 = 1007 and c3 = 1);這時(shí)先不commit
?
session 2:
begin?work;
insert?into?aa(c2,c4)?select?1002,?5?from?dual?where?not?exists(select?*?from?aa?where?c2?=?1002?and?c3?=?0);
commit
?
session1:
commit
?
ok,有點(diǎn)眉目了。在這種情況下,是可以穩(wěn)定復(fù)現(xiàn)的。用戶反映他的業(yè)務(wù)是自動(dòng)提交的。如果兩個(gè)insert來自不同的服務(wù)器,第一次執(zhí)行的時(shí)間很長(zhǎng)還未提交,第二個(gè)就開始執(zhí)行了。也是可能出現(xiàn)的。后來從SQL審計(jì)日志從驗(yàn)證了的確如此。
?
不過客戶去復(fù)現(xiàn)時(shí),卻依然復(fù)現(xiàn)不出來。這時(shí)想到可能是隔離級(jí)別不一樣。
果然,我是在Read committed的場(chǎng)景下復(fù)現(xiàn),客戶是在Repeatable read的場(chǎng)景下復(fù)現(xiàn)。
?
那么,為什么兩個(gè)隔離級(jí)別會(huì)有不同的效果呢?
根本原因是RC隔離級(jí)別保證對(duì)讀取到的記錄加鎖 (記錄鎖);
RR隔離級(jí)別保證對(duì)讀取到的記錄加鎖 (記錄鎖),同時(shí)保證對(duì)讀取的范圍加鎖,新的滿足查詢條件的記錄不能夠插入 (GAP鎖),不存在幻讀現(xiàn)象。
具體是如何加鎖,可以直接看本文最后一個(gè)部分。
?
解決辦法:
1. 給(c2, c3)加唯一索引進(jìn)行約束。(客戶的應(yīng)用場(chǎng)景不支持,因?yàn)闃I(yè)務(wù)只有c2=? and c3=0的狀態(tài)只能出現(xiàn)一條,對(duì)于c3等于其他狀態(tài)值,可以允許多條記錄)
2. 將這個(gè)插入語句設(shè)置為 session級(jí)別的Repeatable read。這種方式對(duì)應(yīng)用的改動(dòng)最小。
?
如果設(shè)置為全局的Repeatable read隔離級(jí)別有什么問題?
1. 鎖等待的范圍擴(kuò)大(增加了GAP鎖),可能更大概率的出現(xiàn)死鎖。
2. 在RR級(jí)別中,通過MVCC機(jī)制,雖然讓數(shù)據(jù)變得可重復(fù)讀,但讀到的數(shù)據(jù)可能是歷史數(shù)據(jù),是不及時(shí)的數(shù)據(jù),不是數(shù)據(jù)庫(kù)當(dāng)前的數(shù)據(jù)!這在一些對(duì)于數(shù)據(jù)的時(shí)效特別敏感的業(yè)務(wù)中,就很可能出問題。
?
?
一些基本知識(shí)點(diǎn):
兩段鎖
數(shù)據(jù)庫(kù)遵循的是兩段鎖協(xié)議,將事務(wù)分成兩個(gè)階段,加鎖階段和解鎖階段(所以叫兩段鎖)
- 加鎖階段:在該階段可以進(jìn)行加鎖操作。在對(duì)任何數(shù)據(jù)進(jìn)行讀操作之前要申請(qǐng)并獲得S鎖(共享鎖,其它事務(wù)可以繼續(xù)加共享鎖,但不能加排它鎖),在進(jìn)行寫操作之前要申請(qǐng)并獲得X鎖(排它鎖,其它事務(wù)不能再獲得任何鎖)。加鎖不成功,則事務(wù)進(jìn)入等待狀態(tài),直到加鎖成功才繼續(xù)執(zhí)行。
- 解鎖階段:當(dāng)事務(wù)釋放了一個(gè)封鎖以后,事務(wù)進(jìn)入解鎖階段,在該階段只能進(jìn)行解鎖操作不能再進(jìn)行加鎖操作。
| begin; | ? |
| insert into test ..... | 加insert對(duì)應(yīng)的鎖 |
| update test set... | 加update對(duì)應(yīng)的鎖 |
| delete from test .... | 加delete對(duì)應(yīng)的鎖 |
| commit; | 事務(wù)提交時(shí),同時(shí)釋放insert、update、delete對(duì)應(yīng)的鎖 |
這種方式雖然無法避免死鎖,但是兩段鎖協(xié)議可以保證事務(wù)的并發(fā)調(diào)度是串行化(串行化很重要,尤其是在數(shù)據(jù)恢復(fù)和備份的時(shí)候)的。
?
事務(wù)的隔離級(jí)別
| 未提交讀(Read uncommitted) | 可能 | 可能 | 可能 |
| 已提交讀(Read committed) | 不可能 | 可能 | 可能 |
| 可重復(fù)讀(Repeatable read) | 不可能 | 不可能 | 可能 |
| 可串行化(Serializable ) | 不可能 | 不可能 | 不可能 |
- 未提交讀(Read Uncommitted):允許臟讀,也就是可能讀取到其他會(huì)話中未提交事務(wù)修改的數(shù)據(jù)
- 提交讀(Read Committed):只能讀取到已經(jīng)提交的數(shù)據(jù)。針對(duì)當(dāng)前讀,RC隔離級(jí)別保證對(duì)讀取到的記錄加鎖 (記錄鎖),存在幻讀現(xiàn)象。
- 可重復(fù)讀(Repeated Read):可重復(fù)讀。在同一個(gè)事務(wù)內(nèi)的查詢都是事務(wù)開始時(shí)刻一致的,InnoDB默認(rèn)級(jí)別。針對(duì)當(dāng)前讀,RR隔離級(jí)別保證對(duì)讀取到的記錄加鎖 (記錄鎖),同時(shí)保證對(duì)讀取的范圍加鎖,新的滿足查詢條件的記錄不能夠插入 (間隙鎖),不存在幻讀現(xiàn)象。
- 串行讀(Serializable):完全串行化的讀,每次讀都需要獲得表級(jí)共享鎖,讀寫相互都會(huì)阻塞。從MVCC并發(fā)控制退化為基于鎖的并發(fā)控制。不區(qū)別快照讀與當(dāng)前讀,所有的讀操作均為當(dāng)前讀,讀加讀鎖 (S鎖),寫加寫鎖 (X鎖)。Serializable隔離級(jí)別下,讀寫沖突,因此并發(fā)度急劇下降,在MySQL/InnoDB下不建議使用。
Read Uncommitted這種級(jí)別,數(shù)據(jù)庫(kù)一般都不會(huì)用,而且任何操作都不會(huì)加鎖,這里就不討論了。
?
?
快照讀VS當(dāng)前讀
在MVCC并發(fā)控制中,讀操作可以分成兩類:快照讀 (snapshot read)與當(dāng)前讀 (current read)??煺兆x,讀取的是記錄的可見版本 (有可能是歷史版本),不用加鎖。當(dāng)前讀,讀取的是記錄的最新版本,并且,當(dāng)前讀返回的記錄,都會(huì)加上鎖,保證其他事務(wù)不會(huì)再并發(fā)修改這條記錄。
在一個(gè)支持MVCC并發(fā)控制的系統(tǒng)中,哪些讀操作是快照讀?哪些操作又是當(dāng)前讀呢?以MySQL InnoDB為例:?
- 快照讀:簡(jiǎn)單的select操作,屬于快照讀,不加鎖。
- select * from table where ?;?
- 當(dāng)前讀:特殊的讀操作,插入/更新/刪除操作,屬于當(dāng)前讀,需要加鎖。
- select * from table where ? lock in share mode;
- select * from table where ? for update;
- insert into table values (…);
- update table set ? where ?;
- delete from table where ?;
所有以上的語句,都屬于當(dāng)前讀,讀取記錄的最新版本。并且,讀取之后,還需要保證其他并發(fā)事務(wù)不能修改當(dāng)前記錄,對(duì)讀取記錄加鎖。其中,除了第一條語句,對(duì)讀取記錄加S鎖 (共享鎖)外,其他的操作,都加的是X鎖 (排它鎖)。?
?
不同隔離級(jí)別,以及對(duì)于不同的索引情況會(huì)如何加鎖?
delete from t1 where id = 10;?
- 組合一:id列是主鍵,RC隔離級(jí)別:?只需要將主鍵上id = 10的記錄加上X鎖即可
- 組合二:id列是二級(jí)唯一索引,RC隔離級(jí)別:?需要加兩個(gè)X鎖,一個(gè)對(duì)應(yīng)于id unique索引上的id = 10的記錄,另一把鎖對(duì)應(yīng)于聚簇索引上的[name=’d’,id=10]的記錄。
- 組合三:id列是二級(jí)非唯一索引,RC隔離級(jí)別:對(duì)應(yīng)的所有滿足SQL查詢條件的記錄,都會(huì)被加鎖。同時(shí),這些記錄在主鍵索引上的記錄,也會(huì)被加鎖。
- 組合四:id列上沒有索引,RC隔離級(jí)別:SQL會(huì)走聚簇索引的全掃描進(jìn)行過濾,由于過濾是由MySQL Server層面進(jìn)行的。因此每條記錄,無論是否滿足條件,都會(huì)被加上X鎖。但是,為了效率考量,MySQL做了優(yōu)化,對(duì)于不滿足條件的記錄,會(huì)在判斷后放鎖,最終持有的,是滿足條件的記錄上的鎖,但是不滿足條件的記錄上的加鎖/放鎖動(dòng)作不會(huì)省略。
- 聚簇索引上所有的記錄,都被加上了X鎖。無論記錄是否滿足條件,全部被加上X鎖。這個(gè)鎖的效果和表鎖有什么區(qū)別?rc隔離級(jí)別下,有區(qū)別,記錄仍舊可以插入。rr下,功能上無區(qū)別。但是innodb不會(huì)主動(dòng)升級(jí)表鎖。
- 為什么不是只在滿足條件的記錄上加鎖呢?這是由于MySQL的實(shí)現(xiàn)決定的。如果一個(gè)條件無法通過索引快速過濾,那么存儲(chǔ)引擎層面(innodb)就會(huì)將所有記錄加鎖后返回,然后由MySQL Server層進(jìn)行過濾。因此也就把所有的記錄,都鎖上了。
- 在5.6后支持了Index Condition Pushdown, 可以在innodb層進(jìn)行過濾。
- 組合五:id列是主鍵,RR隔離級(jí)別:加鎖與組合一[id主鍵,Read Committed]一致。
- 組合六:id列是二級(jí)唯一索引,RR隔離級(jí)別:?與組合二[id唯一索引,Read Committed]一致。
- 組合七:id列是二級(jí)非唯一索引,RR隔離級(jí)別:首先,通過id索引定位到第一條滿足查詢條件的記錄,加記錄上的X鎖,加GAP上的GAP鎖,然后加主鍵聚簇索引上的記錄X鎖,然后返回;然后讀取下一條,重復(fù)進(jìn)行。直至進(jìn)行到第一條不滿足條件的記錄[11,f],此時(shí),不需要加記錄X鎖,但是仍舊需要加GAP鎖,最后返回結(jié)束。
- 組合八:id列上沒有索引,RR隔離級(jí)別:在Repeatable Read隔離級(jí)別下,如果進(jìn)行全表掃描的當(dāng)前讀,那么會(huì)鎖上表中的所有記錄,同時(shí)會(huì)鎖上聚簇索引內(nèi)的所有GAP,杜絕所有的并發(fā) 更新/刪除/插入 操作。當(dāng)然,也可以通過觸發(fā)semi-consistent read,來緩解加鎖開銷與并發(fā)影響,但是semi-consistent read本身也會(huì)帶來其他問題,不建議使用。
- 組合九:Serializable隔離級(jí)別:?在MySQL/InnoDB中,所謂的讀不加鎖,并不適用于所有的情況,而是隔離級(jí)別相關(guān)的。Serializable隔離級(jí)別,讀不加鎖就不再成立,所有的讀操作,都是當(dāng)前讀。
?
?
轉(zhuǎn)載于:https://www.cnblogs.com/yuyue2014/p/4747018.html
總結(jié)
以上是生活随笔為你收集整理的值班问题:insert语句插入了两条数据?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。