事務(wù)并發(fā)執(zhí)行遇到的問(wèn)題
- 臟讀(未提交讀)
- 不可重復(fù)讀(已提交讀)
- 幻讀(讀出新紀(jì)錄)
事務(wù)隔離級(jí)別
| 隔離級(jí)別 | 臟讀 | 不可重復(fù)讀 | 幻讀 |
|---|---|---|---|
| 未提交讀 | 可能 | 可能 | 可能 |
| 已提交讀 | —— | 可能 | 可能 |
| 可重復(fù)讀 | —— | —— | —— |
| 可串行化 | —— | —— | —— |
mysql在可重復(fù)讀的級(jí)別下,極大的程度上避免了幻讀。
版本鏈
對(duì)于InnoDB引擎來(lái)說(shuō),每個(gè)主鍵索引記錄中包含了兩個(gè)隱藏列
| 隱藏列 | |
|---|---|
| trx_id | 記錄當(dāng)前正在操作此條記錄的事務(wù)id |
| roll_pointer | 修改時(shí),記錄舊的版本到undo日志中, 當(dāng)前列就是指向舊版本的指針,以便找到修改前的信息 |
示例
- 創(chuàng)建一張表
CREATE TABLE teacher ( number INT,
name VARCHAR(100), domain varchar(100), PRIMARY KEY (number)
) Engine=InnoDB CHARSET=utf8;
- 新增一條記錄,并且當(dāng)前插入的事物id為60
INSERT INTO teacher VALUES(1, 'Jack', '源碼系列');
此刻生成一條記錄,如下圖所示

insert
- 之后有兩個(gè)80、120事務(wù)更新
| trx_id(80) | trx_id(120) |
|---|---|
| BEGIN | |
| BEGIN | |
| UPDATE teacher SET name = 'Mark' WHERE number = 1; | |
| UPDATE teacher SET name = 'James' WHERE number = 1; | |
| COMMIT | |
| UPDATE teacher SET name = 'King' WHERE number = 1; | |
| UPDATE teacher SET name = '大飛' WHERE number = 1; | |
| COMMIT |
每次記錄修改都有一條undo日志,每條undo日志都會(huì)一個(gè)roll_pointer屬性??梢詫⑦@些undo日志串成鏈表,我們把這種鏈表稱為版本鏈。

msyql版本鏈
對(duì)該記錄每次更新后,都會(huì)將舊值放到一條 undo 日志中,就算是該記錄的 一個(gè)舊版本,隨著更新次數(shù)的增多,所有的版本都會(huì)被 roll_pointer 屬性連接成 一個(gè)鏈表,我們把這個(gè)鏈表稱之為版本鏈
ReadView
InnoDB 提出了一個(gè) ReadView 的概念, 包含 4 個(gè)比較重要的內(nèi)容:
| id | 說(shuō)明 |
|---|---|
| m_ids | 生成 ReadView 時(shí)當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)的事務(wù) id 列表 |
| min_trx_id | 生成 ReadView 時(shí)當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)中最小的事務(wù) id,也就是 m_ids 中的最小值。 |
| max_trx_id | 生成 ReadView 時(shí)系統(tǒng)中應(yīng)該分配給下一個(gè)事務(wù)的 id 值 |
| creator_trx_id | 生成該 ReadView 的事務(wù)的事務(wù) id |
訪問(wèn)某條記錄時(shí),使用ReadView,遍歷版本鏈,通過(guò)下面規(guī)則判斷某個(gè)節(jié)點(diǎn)數(shù)據(jù)是否可以訪問(wèn)。
- 如果被訪問(wèn)的 trx_id 屬性值與 ReadView 中的 creator_trx_id 值相同, 意味著當(dāng)前事務(wù)在訪問(wèn)它自己修改過(guò)的記錄,所以該版本可以被當(dāng)前事務(wù)訪問(wèn)。
- 如果被訪問(wèn)版本的 trx_id 屬性值小于 ReadView 中的 min_trx_id 值,表明 生成該版本的事務(wù)在當(dāng)前事務(wù)生成 ReadView 前已經(jīng)提交,所以該版本可以被當(dāng) 前事務(wù)訪問(wèn)。
- 如果被訪問(wèn)版本的 trx_id 屬性值大于或等于 ReadView 中的 max_trx_id 值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成 ReadView 后才開啟,所以該版本不 可以被當(dāng)前事務(wù)訪問(wèn)。
- 如果被訪問(wèn)版本的 trx_id 屬性值在 ReadView 的 min_trx_id 和 max_trx_id 之間(min_trx_id <= trx_id < max_trx_id),那就需要判斷一下 trx_id 屬性值是不是在 m_ids 列表中,如果在,說(shuō)明創(chuàng)建 ReadView 時(shí)生成該版本的事務(wù)還是活躍的, 該版本不可以被訪問(wèn);如果不在,說(shuō)明創(chuàng)建 ReadView 時(shí)生成該版本的事務(wù)已經(jīng) 被提交,該版本可以被訪問(wèn)。
- 如果某個(gè)版本的數(shù)據(jù)對(duì)當(dāng)前事務(wù)不可見的話,那就順著版本鏈找到下一 個(gè)版本的數(shù)據(jù),繼續(xù)按照上邊的步驟判斷可見性,依此類推,直到版本鏈中的最 后一個(gè)版本。如果最后一個(gè)版本也不可見的話,那么就意味著該條記錄對(duì)該事務(wù) 完全不可見,查詢結(jié)果就不包含該記錄。
在 MySQL 中,已提交讀(RC)和可重復(fù)讀(RR)隔離級(jí)別的的一個(gè)非 常大的區(qū)別就是它們生成 ReadView 的時(shí)機(jī)不同。
- 已提交讀(RC)在每次讀取數(shù)據(jù)前都生成一個(gè) ReadView。
- 可重復(fù)讀(RR)在第一次讀取數(shù)據(jù)時(shí)生成一個(gè) ReadView。
mysql默認(rèn)的事務(wù)隔離級(jí)別是可重復(fù)讀(RC)
mysql可重復(fù)讀(RR)基本解決了幻讀問(wèn)題,那什么情況下會(huì)出現(xiàn)幻讀問(wèn)題呢。
- 假設(shè)有T1,T2,兩個(gè)事務(wù),T1先開啟事務(wù)。
- T2插入一條記錄,并提交事務(wù)
- T1查詢T2插入的記錄,很明顯查詢不到。
- 如果此刻T1更新了T2插入的那條記錄
- 此刻T1再去查詢那條記錄,這時(shí)是可以查到的,這就是幻讀
這種情況在實(shí)際生產(chǎn)并不多。