事務(wù)是由 MySQL 的引擎來(lái)實(shí)現(xiàn)的,我們常見的 InnoDB 引擎它是支持事務(wù)的。不過(guò)并不是所有的引擎都能支持事務(wù),比如 MySQL 原生的 MyISAM 引擎就不支持事務(wù),也正是這樣,所以大多數(shù) MySQL 的引擎都是用 InnoDB。事務(wù)看起來(lái)感覺簡(jiǎn)單,但是要實(shí)現(xiàn)事務(wù)必須要遵守 4 個(gè)特性,分別如下:
MySQL 服務(wù)端是允許多個(gè)客戶端連接的,這意味著 MySQL 會(huì)出現(xiàn)同時(shí)處理多個(gè)事務(wù)的情況。那么在同時(shí)處理多個(gè)事務(wù)的時(shí)候,就可能出現(xiàn)臟讀(dirty read)、不可重復(fù)讀(non-repeatable read)、幻讀(phantom read)的問(wèn)題。接下來(lái),通過(guò)舉例子給大家說(shuō)明,這些問(wèn)題是如何發(fā)生的。
臟讀
如果一個(gè)事務(wù)「讀到」了另一個(gè)「未提交事務(wù)修改過(guò)的數(shù)據(jù)」,就意味著發(fā)生了「臟讀」現(xiàn)象。舉個(gè)栗子。假設(shè)有 A 和 B 這兩個(gè)事務(wù)同時(shí)在處理,事務(wù) A 先開始從數(shù)據(jù)庫(kù)中讀取小林的余額數(shù)據(jù),然后再執(zhí)行更新操作,如果此時(shí)事務(wù) A 還沒有提交事務(wù),而此時(shí)正好事務(wù) B 也從數(shù)據(jù)庫(kù)中讀取小林的余額數(shù)據(jù),那么事務(wù) B 讀取到的余額數(shù)據(jù)是剛才事務(wù) A 更新后的數(shù)據(jù),即使沒有提交事務(wù)。因?yàn)槭聞?wù) A 是還沒提交事務(wù)的,也就是它隨時(shí)可能發(fā)生回滾操作,如果在上面這種情況事務(wù) A 發(fā)生了回滾,那么事務(wù) B 剛才得到的數(shù)據(jù)就是過(guò)期的數(shù)據(jù),這種現(xiàn)象就被稱為臟讀。
不可重復(fù)讀
在一個(gè)事務(wù)內(nèi)多次讀取同一個(gè)數(shù)據(jù),如果出現(xiàn)前后兩次讀到的數(shù)據(jù)不一樣的情況,就意味著發(fā)生了「不可重復(fù)讀」現(xiàn)象。舉個(gè)栗子。假設(shè)有 A 和 B 這兩個(gè)事務(wù)同時(shí)在處理,事務(wù) A 先開始從數(shù)據(jù)庫(kù)中讀取小林的余額數(shù)據(jù),然后繼續(xù)執(zhí)行代碼邏輯處理,在這過(guò)程中如果事務(wù) B 更新了這條數(shù)據(jù),并提交了事務(wù),那么當(dāng)事務(wù) A 再次讀取該數(shù)據(jù)時(shí),就會(huì)發(fā)現(xiàn)前后兩次讀到的數(shù)據(jù)是不一致的,這種現(xiàn)象就被稱為不可重復(fù)讀。
幻讀
在一個(gè)事務(wù)內(nèi)多次查詢某個(gè)符合查詢條件的「記錄數(shù)量」,如果出現(xiàn)前后兩次查詢到的記錄數(shù)量不一樣的情況,就意味著發(fā)生了「幻讀」現(xiàn)象。舉個(gè)栗子。假設(shè)有 A 和 B 這兩個(gè)事務(wù)同時(shí)在處理,事務(wù) A 先開始從數(shù)據(jù)庫(kù)查詢賬戶余額大于 100 萬(wàn)的記錄,發(fā)現(xiàn)共有 5 條,然后事務(wù) B 也按相同的搜索條件也是查詢出了 5 條記錄。接下來(lái),事務(wù) A 插入了一條余額超過(guò) 100 萬(wàn)的賬號(hào),并提交了事務(wù),此時(shí)數(shù)據(jù)庫(kù)超過(guò) 100 萬(wàn)余額的賬號(hào)個(gè)數(shù)就變?yōu)?6。然后事務(wù) B 再次查詢賬戶余額大于 100 萬(wàn)的記錄,此時(shí)查詢到的記錄數(shù)量有 6 條,發(fā)現(xiàn)和前一次讀到的記錄數(shù)量不一樣了,就感覺發(fā)生了幻覺一樣,這種現(xiàn)象就被稱為幻讀。
所以,要解決臟讀現(xiàn)象,就要升級(jí)到「讀提交」以上的隔離級(jí)別;要解決不可重復(fù)讀現(xiàn)象,就要升級(jí)到「可重復(fù)讀」的隔離級(jí)別。不過(guò),要解決幻讀現(xiàn)象不建議將隔離級(jí)別升級(jí)到「串行化」,因?yàn)檫@樣會(huì)導(dǎo)致數(shù)據(jù)庫(kù)在并發(fā)事務(wù)時(shí)性能很差。InnoDB 引擎的默認(rèn)隔離級(jí)別雖然是「可重復(fù)讀」,但是它通過(guò)next-key lock 鎖(行鎖和間隙鎖的組合)來(lái)鎖住記錄之間的“間隙”和記錄本身,防止其他事務(wù)在這個(gè)記錄之間插入新的記錄,這樣就避免了幻讀現(xiàn)象。接下里,舉個(gè)具體的例子來(lái)說(shuō)明這四種隔離級(jí)別,有一張賬戶余額表,里面有一條記錄:然后有兩個(gè)并發(fā)的事務(wù),事務(wù) A 只負(fù)責(zé)查詢余額,事務(wù) B 則會(huì)將我的余額改成 200 萬(wàn),下面是按照時(shí)間順序執(zhí)行兩個(gè)事務(wù)的行為:在不同隔離級(jí)別下,事務(wù) A 執(zhí)行過(guò)程中查詢到的余額可能會(huì)不同:
在「讀未提交」隔離級(jí)別下,事務(wù) B 修改余額后,雖然沒有提交事務(wù),但是此時(shí)的余額已經(jīng)可以被事務(wù) A 看見了,于是事務(wù) A 中余額 V1 查詢的值是 200 萬(wàn),余額 V2、V3 自然也是 200 萬(wàn)了;
在「讀提交」隔離級(jí)別下,事務(wù) B 修改余額后,因?yàn)闆]有提交事務(wù),所以事務(wù) A 中余額 V1 的值還是 100 萬(wàn),等事務(wù) B 提交完后,最新的余額數(shù)據(jù)才能被事務(wù) A 看見,因此額 V2、V3 都是 200 萬(wàn);
在「可重復(fù)讀」隔離級(jí)別下,事務(wù) A 只能看見啟動(dòng)事務(wù)時(shí)的數(shù)據(jù),所以余額 V1、余額 V2 的值都是 100 萬(wàn),當(dāng)事務(wù) A 提交事務(wù)后,就能看見最新的余額數(shù)據(jù)了,所以余額 V3 的值是 200 萬(wàn);
在「串行化」隔離級(jí)別下,事務(wù) B 在執(zhí)行將余額 100 萬(wàn)修改為 200 萬(wàn)時(shí),由于此前事務(wù) A 執(zhí)行了讀操作,這樣就發(fā)生了讀寫沖突,于是就會(huì)被鎖住,直到事務(wù) A 提交后,事務(wù) B 才可以繼續(xù)執(zhí)行,所以從 A 的角度看,余額 V1、V2 的值是 100 萬(wàn),余額 V3 的值是 200萬(wàn)。
了解完這兩個(gè)知識(shí)點(diǎn)后,就可以跟大家說(shuō)說(shuō)可重復(fù)讀隔離級(jí)別是如何實(shí)現(xiàn)的。假設(shè)事務(wù) A 和 事務(wù) B 差不多同一時(shí)刻啟動(dòng),那這兩個(gè)事務(wù)創(chuàng)建的 Read View 如下:事務(wù) A 和 事務(wù) B 的 Read View 具體內(nèi)容如下:
在事務(wù) A 的 Read View 中,它的事務(wù) id 是 51,由于與事務(wù) B 同時(shí)啟動(dòng),所以此時(shí)活躍的事務(wù)的事務(wù) id 列表是 51 和 52,活躍的事務(wù) id 中最小的事務(wù) id 是事務(wù) A 本身,下一個(gè)事務(wù) id 應(yīng)該是 53。
在事務(wù) B 的 Read View 中,它的事務(wù) id 是 52,由于與事務(wù) A 同時(shí)啟動(dòng),所以此時(shí)活躍的事務(wù)的事務(wù) id 列表是 51 和 52,活躍的事務(wù) id 中最小的事務(wù) id 是事務(wù) A,下一個(gè)事務(wù) id 應(yīng)該是 53。
然后讓事務(wù) A 去讀賬戶余額為 100 萬(wàn)的記錄,在找到記錄后,它會(huì)先看這條記錄的 trx_id,此時(shí)發(fā)現(xiàn) trx_id 為 50,通過(guò)和事務(wù) A 的 Read View 的 m_ids 字段發(fā)現(xiàn),該記錄的事務(wù) id 并不在活躍事務(wù)的列表中,并且小于事務(wù) A 的事務(wù) id,這意味著,這條記錄的事務(wù)早就在事務(wù) A 前提交過(guò)了,所以該記錄對(duì)事務(wù) A 可見,也就是事務(wù) A 可以獲取到這條記錄。接著,事務(wù) B 通過(guò) update 語(yǔ)句將這條記錄修改了,將小林的余額改成 200 萬(wàn),這時(shí) MySQL 會(huì)記錄相應(yīng)的 undo log,并以鏈表的方式串聯(lián)起來(lái),形成版本鏈,如下圖:你可以在上圖的「記錄字段」看到,由于事務(wù) B 修改了該記錄,以前的記錄就變成舊版本記錄了,于是最新記錄和舊版本記錄通過(guò)鏈表的方式串起來(lái),而且最新記錄的 trx_id 是事務(wù) B 的事務(wù) id。然后如果事務(wù) A 再次讀取該記錄,發(fā)現(xiàn)這條記錄的 trx_id 為 52,比自己的事務(wù) id 還大,并且比下一個(gè)事務(wù) id 53 小,這意味著,事務(wù) A 讀到是和自己同時(shí)啟動(dòng)事務(wù)的事務(wù) B 修改的數(shù)據(jù),這時(shí)事務(wù) A 并不會(huì)讀取這條記錄,而是沿著 undo log 鏈條往下找舊版本的記錄,直到找到 trx_id 等于或者小于事務(wù) A 的事務(wù) id 的第一條記錄,所以事務(wù) A 再一次讀取到 trx_id 為 50 的記錄,也就是小林余額是 100 萬(wàn)的這條記錄?!缚芍貜?fù)讀」隔離級(jí)別就是在啟動(dòng)時(shí)創(chuàng)建了 Read View,然后在事務(wù)期間讀取數(shù)據(jù)的時(shí)候,在找到數(shù)據(jù)后,先會(huì)將該記錄的 trx_id 和該事務(wù)的 Read View 里的字段做個(gè)比較:
「讀提交」隔離級(jí)別是在每個(gè) select 都會(huì)生成一個(gè)新的 Read View,也意味著,事務(wù)期間的多次讀取同一條數(shù)據(jù),前后兩次讀的數(shù)據(jù)可能會(huì)出現(xiàn)不一致,因?yàn)榭赡苓@期間另外一個(gè)事務(wù)修改了該記錄,并提交了事務(wù)。那讀提交隔離級(jí)別是怎么實(shí)現(xiàn)呢?我們還是以前面的例子來(lái)聊聊。假設(shè)事務(wù) A 和 事務(wù) B 差不多同一時(shí)刻啟動(dòng),然后事務(wù) B 將小林的賬戶余額修改成了 200 萬(wàn),但是事務(wù) B 還未提交,這時(shí)事務(wù) A 讀到的數(shù)據(jù),應(yīng)該還是小林賬戶余額為 100 萬(wàn)的數(shù)據(jù),那具體怎么做到的呢?事務(wù) A 在找到小林這條記錄時(shí),會(huì)看這條記錄的 trx_id,發(fā)現(xiàn)和事務(wù) A 的 Read View 中的 creator_trx_id 要大,而且還在 m_ids 列表里,說(shuō)明這條記錄被事務(wù) B 修改過(guò),而且還可以知道事務(wù) B 并沒有提交事務(wù),因?yàn)槿绻峤涣耸聞?wù),那么這條記錄的 trx_id 就不會(huì)在 m_ids 列表里。因此,事務(wù) A 不能讀取該記錄,而是沿著 undo log 鏈條往下找。當(dāng)事務(wù) B 修改數(shù)據(jù)并提交了事務(wù)后,這時(shí)事務(wù) A 讀到的數(shù)據(jù),就是小林賬戶余額為 200 萬(wàn)的數(shù)據(jù),那具體怎么做到的呢?事務(wù) A 在找到小林這條記錄時(shí),會(huì)看這條記錄的 trx_id,發(fā)現(xiàn)和事務(wù) A 的 Read View 中的 creator_trx_id 要大,而且不在 m_ids 列表里,說(shuō)明該記錄的 trx_id 的事務(wù)是已經(jīng)提交過(guò)的了,于是事務(wù) A 就可以讀取這條記錄,這也就是所謂的讀已提交機(jī)制。