1. 事務(wù)隔離級(jí)別
四種隔離級(jí)別及對(duì)應(yīng)問題的可能性养叛。
隔離級(jí)別 | 臟讀 | 不可重復(fù)讀 | 幻讀 |
---|---|---|---|
讀未提交 | 可能 | 可能 | 可能 |
讀已提交 | 不可能 | 可能 | 可能 |
可重復(fù)讀 | 不可能 | 不可能 | 可能 |
串行化 | 不可能 | 不可能 | 不可能 |
2. 快照讀(非鎖定讀, 普通select語句)
一致性非鎖定讀是通過MVCC來讀取數(shù)據(jù)庫中對(duì)當(dāng)前事務(wù)而言可讀版本中最新數(shù)據(jù)水评。在查詢時(shí),有其他事務(wù)正在完成寫入具壮,那么不會(huì)等待行鎖的釋放匾旭,而是讀取舊一點(diǎn)版本的數(shù)據(jù)溯壶。
2.1 undo log
innodb中及皂,當(dāng)事務(wù)提交時(shí)甫男,需要先將該事務(wù)的所有操作日志寫入到重做日志文件且改,然后等提交操作完成,事務(wù)才算完成板驳。這里的重做日志文件包含了兩部分:redo log和undo log又跛。 redo log被用來保證事物的持久性,undo log用來幫助實(shí)現(xiàn)事務(wù)回滾和MVCC若治。這里重點(diǎn)講一下undo log, 當(dāng)事務(wù)回滾時(shí)慨蓝,利用undo邏輯地將數(shù)據(jù)庫恢復(fù)到之前的狀態(tài)。
那么如何通過undo 來回滾和實(shí)現(xiàn)MVCC呢端幼。 聚簇索引記錄中都包含兩個(gè)必要的隱藏列:
- trx_id:每次對(duì)某條聚簇索引記錄進(jìn)行改動(dòng)時(shí)礼烈,都會(huì)把對(duì)應(yīng)的事務(wù)id賦值給trx_id隱藏列。
- roll_pointer:每次對(duì)某條聚簇索引記錄進(jìn)行改動(dòng)時(shí)婆跑,都會(huì)把舊的版本寫入到undo日志中此熬,然后這個(gè)隱藏列就相當(dāng)于一個(gè)指針,可以通過它來找到該記錄修改前的信息滑进。
每次對(duì)記錄進(jìn)行改動(dòng)時(shí)犀忱,生成對(duì)應(yīng)的undo log, undo log中同樣有roll pointer扶关,指向了更早的一個(gè)版本阴汇。于是就形成了一個(gè)鏈表。
事務(wù)回滾時(shí)节槐,邏輯地刪除該版本鏈中對(duì)應(yīng)trx_id的數(shù)據(jù)就行了搀庶。
2.2 MVCC
read uncommitted級(jí)別下, 直接讀取版本鏈中的頭節(jié)點(diǎn)就好了铜异。
-
read committed級(jí)別下哥倔, MVCC在每個(gè)select執(zhí)行時(shí),先創(chuàng)建一個(gè)read view熙掺。 read view 包含了以下信息:
- 當(dāng)前事務(wù)id: cur_trx_id
- 當(dāng)前系統(tǒng)中最小的活躍事務(wù)id: min_active_trx_id
- 當(dāng)前系統(tǒng)中最大事務(wù)id: max_trx_id
通過read view未斑,可以確定哪些事務(wù)寫入的數(shù)據(jù)是本次select可讀的:readable_trx = (0, min_active_trx_id) U (min_active, max_trx_id)區(qū)間內(nèi)非活躍的事務(wù) U 當(dāng)前事務(wù)
select讀取時(shí),通過版本鏈币绩,讀取出trx_id在readable_trx中且版本最新的記錄蜡秽,作為返回值府阀。由于讀取的要么是已提交事務(wù)的寫入,要么是當(dāng)前事務(wù)的寫入芽突,因此避免了臟讀试浙,然而由于每次select都會(huì)生成read view, 可讀取版本可能會(huì)不同,因此無法避免不可重復(fù)讀寞蚌。
repeatable read 級(jí)別下田巴, 在事務(wù)中的第一個(gè)select時(shí),去創(chuàng)建read view挟秤。確定當(dāng)前事務(wù)中select的可讀版本壹哺。之后每次select時(shí),都根據(jù)該read view 去讀取可讀且版本最新的數(shù)據(jù)艘刚。從而保證了該事務(wù)中的可重復(fù)讀管宵。
serializable級(jí)別下,串行化攀甚,那么就無法通過版本連來控制了箩朴。讀加共享鎖,寫加排他鎖秋度,讀寫互斥炸庞。
3. 當(dāng)前讀(鎖定讀)
在RC和RR隔離級(jí)別下,快照讀總是在當(dāng)前事務(wù)中先創(chuàng)建read view,然后讀出可讀版本的最新數(shù)據(jù)荚斯。然而在某些場(chǎng)景下埠居,我們希望的是,在當(dāng)前事務(wù)中要讀取數(shù)據(jù)時(shí)鲸拥,如果有其他事務(wù)正在寫入與查詢相關(guān)的數(shù)據(jù)拐格,我們先等他寫入完成,提交后獲取到讀取的機(jī)會(huì)刑赶,然后讀出寫入后的最新數(shù)據(jù)捏浊。這種模式下,快照讀就無能為力了撞叨。這是一個(gè)很典型的讀寫問題金踪,innodb為了解決這種問題,采用了讀寫鎖的解決辦法牵敷。
- read uncommitted級(jí)別下胡岔, 讀寫均不互斥,假設(shè)事務(wù)b枷餐,更新某條記錄靶瘸,此時(shí)事務(wù)a 去select for update/ in share mode, 由于未作任何同步互斥, 此時(shí)事務(wù)a即可讀取到b對(duì)數(shù)據(jù)的寫入,如果事務(wù)b回滾怨咪,那么可能帶來很多負(fù)面的影響屋剑。
- read committed級(jí)別下鎖定當(dāng)前記錄。臟讀是由于一個(gè)事務(wù)中對(duì)數(shù)據(jù)的寫入诗眨,在提交之前被另一個(gè)事務(wù)讀取到唉匾。那么解決思路就是寫入時(shí)對(duì)該數(shù)據(jù)加X鎖,讀取時(shí)根據(jù)需要加S鎖(select ... in share mode)或者X鎖(select ...for update)匠楚。也就是所謂的record lock巍膘。假設(shè)數(shù)據(jù)庫內(nèi)有非聚簇索引[1,3, 5, 5, 5, 7 ], 事務(wù)a在讀取索引為5的記錄時(shí)加record lock,此時(shí)另一個(gè)事務(wù)b想要修改索引為5的數(shù)據(jù)芋簿,發(fā)生阻塞峡懈。直到事務(wù)a提交事務(wù),釋放鎖之后益咬,事務(wù)b繼續(xù)執(zhí)行才能訪問逮诲。
但是RC級(jí)別下帜平,無法解決幻讀的問題幽告,假設(shè)事務(wù)b是要插入一條索引為5的記錄,根據(jù)b+樹裆甩,它可以在(3,5), 5冗锁,(5,7)之間的任何位置進(jìn)行插入,此時(shí)僅僅鎖住了5這個(gè)點(diǎn)嗤栓,那么仍然可以在(3,5),(5,7)兩個(gè)區(qū)間的中間插入冻河。
事務(wù)a | 事務(wù)b |
---|---|
begin; | begin; |
select count(1) from xx where sec_id = 5; (結(jié)果:3) | |
insert into xxx set ... sec_id =5 | |
commit | |
select count(1) from xx where sec_id = 5; (結(jié)果:4) | |
... |
repeatable read級(jí)別下,使用next-key lock的方式對(duì)索引加鎖茉帅,對(duì)非唯一索引而言叨叙,它不僅鎖住記錄本身(record lock),而且還對(duì)(3,5), (5,7)兩個(gè)區(qū)間加鎖(gap lock)堪澎。最終的效果就是擂错,在(3,7)區(qū)間內(nèi)的任何寫入樱蛤,都將被阻塞钮呀。對(duì)于唯一索引而言,只需要鎖住當(dāng)前記錄本身即可昨凡,因此next-key-lock降級(jí)為record lock爽醋。因此鎖定讀下的RR級(jí)別,避免了幻讀的問題便脊。