事務有哪些特性?#
MyISAM 引擎就不支持事務,事務必須要遵守 4 個特性:
- 原子性(Atomicity):一個事務中的所有操作,要麼全部完成,要麼全部不完成,不會結束在中間某個環節,而且事務在執行過程中發生錯誤,會被回滾到事務開始前的狀態。
- 一致性(Consistency):是指事務操作前和操作後,數據滿足完整性約束,數據庫保持一致性狀態。
- 隔離性(Isolation):數據庫允許多個並發事務同時對其數據進行讀寫和修改的能力,隔離性可以防止多個事務並發執行時由於交叉執行而導致數據的不一致,因為多個事務同時使用相同的數據時,不會相互干擾,每個事務都有一個完整的數據空間,對其他並發事務是隔離的。
- 持久性(Durability):事務處理結束後,對數據的修改就是永久的,即便系統故障也不會丟失。
InnoDB 引擎通過什麼技術來保證事務的這四個特性的呢?
- 持久性是通過 redo log (重做日誌)來保證的;
- 原子性是通過 undo log(回滾日誌) 來保證的;
- 隔離性是通過 MVCC(多版本並發控制)或鎖機制來保證的;
- 一致性則是通過持久性 + 原子性 + 隔離性來保證;
並行事務會引發什麼問題?#
在同時處理多個事務的時候,就可能出現髒讀(dirty read)、不可重複讀(non-repeatable read)、幻讀(phantom read)
- 髒讀:如果一個事務「讀到」了另一個「未提交事務修改過的數據」,就意味著發生了「髒讀」現象。
- 不可重複讀:在一個事務內多次讀取同一個數據,如果出現前後兩次讀到的數據不一樣的情況,就意味著發生了「不可重複讀」現象。
- 幻讀:在一個事務內多次查詢某個符合查詢條件的「記錄數量」,如果出現前後兩次查詢到的記錄數量不一樣的情況,就意味著發生了「幻讀」現象。
事務的隔離級別有哪些?#
SQL 標準提出了四種隔離級別來規避這些現象,隔離級別越高,性能效率就越低,這四個隔離級別如下:
- 讀未提交(read uncommitted),指一個事務還沒提交時,它做的變更就能被其他事務看到;
- 讀提交(read committed),指一個事務提交之後,它做的變更才能被其他事務看到;
- 可重複讀(repeatable read),指一個事務執行過程中看到的數據,一直跟這個事務啟動時看到的數據是一致的,MySQL InnoDB 引擎的默認隔離級別;
- 串行化(serializable );會對記錄加上讀寫鎖,在多個事務對這條記錄進行讀寫操作時,如果發生了讀寫衝突的時候,後訪問的事務必須等前一個事務執行完成,才能繼續執行;
MySQL InnoDB 引擎的默認隔離級別雖然是「可重複讀」,但是它很大程度上避免幻讀現象(並不是完全解決了),解決方案有兩種:
- 針對快照讀(普通 select 語句),是通過 MVCC 方式解決了幻讀,因為可重複讀隔離級別下,事務執行過程中看到的數據,一直跟這個事務啟動時看到的數據是一致的,即使中途有其他事務插入了一條數據,是查詢不出來這條數據的,所以就很好了避免幻讀問題。
- 針對當前讀(select ... for update 等語句),是通過 next-key lock(記錄鎖 + 間隙鎖)方式解決了幻讀,因為當執行 select ... for update 語句的時候,會加上 next-key lock,如果有其他事務在 next-key lock 鎖範圍內插入了一條記錄,那麼這個插入語句就會被阻塞,無法成功插入,所以就很好了避免幻讀問題。
兩個發生幻讀場景的例子
第一個例子:對於快照讀, MVCC 並不能完全避免幻讀現象。因為當事務 A 更新了一條事務 B 插入的記錄,那麼事務 A 前後兩次查詢的記錄條目就不一樣了,所以就發生幻讀。
第二個例子:對於當前讀,如果事務開啟後,並沒有執行當前讀,而是先快照讀,然後這期間如果其他事務插入了一條記錄,那麼事務後續使用當前讀進行查詢的時候,就會發現兩次查詢的記錄條目就不一樣了,所以就發生幻讀。
所以,MySQL 可重複讀隔離級別並沒有徹底解決幻讀,只是很大程度上避免了幻讀現象的發生。
要避免這類特殊場景下發生幻讀的現象的話,就是盡量在開啟事務之後,馬上執行 select ... for update 這類當前讀的語句,因為它會對記錄加 next-key lock,從而避免其他事務插入一條新記錄。
如何通過 Read View 實現 MVCC#
Read View 到底是個什麼東西?#
Read View 的四個字段
知道了 Read View 的字段,我們還需要了解聚簇索引記錄中的兩個隱藏列。
對於使用 InnoDB 存儲引擎的數據庫表,它的聚簇索引記錄中都包含下面兩個隱藏列:
- trx_id,當一個事務對某條聚簇索引記錄進行改動時,就會把該事務的事務 id 記錄在 trx_id 隱藏列裡;
- roll_pointer,每次對某條聚簇索引記錄進行改動時,都會把舊版本的記錄寫入到 undo 日誌中,然後這個隱藏列是個指針,指向每一個舊版本記錄,於是就可以通過它找到修改前的記錄。
在創建 Read View 後,我們可以將記錄中的 trx_id 劃分這三種情況:
一個事務去訪問記錄的時候,除了自己的更新記錄總是可見之外,還有這幾種情況:
- 如果記錄的 trx_id 值小於 Read View 中的 min_trx_id 值,表示這個版本的記錄是在創建 Read View 前已經提交的事務生成的,所以該版本的記錄對當前事務可見。
- 如果記錄的 trx_id 值大於等於 Read View 中的 max_trx_id 值,表示這個版本的記錄是在創建 Read View 後才啟動的事務生成的,所以該版本的記錄對當前事務不可見。
- 如果記錄的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之間,需要判斷 trx_id 是否在 m_ids 列表中:
- 如果記錄的 trx_id 在 m_ids 列表中,表示生成該版本記錄的活躍事務依然活躍著(還沒提交事務),所以該版本的記錄對當前事務不可見。
- 如果記錄的 trx_id 不在 m_ids 列表中,表示生成該版本記錄的活躍事務已經被提交,所以該版本的記錄對當前事務可見。
這種通過「版本鏈」來控制並發事務訪問同一個記錄時的行為就叫 MVCC(多版本並發控制)。
對於「讀提交」和「可重複讀」隔離級別的事務來說,它們是通過 Read View 來實現的,它們的區別在於創建 Read View 的時機不同:
- 「讀提交」隔離級別是在每個 select 都會生成一個新的 Read View,也意味著,事務期間的多次讀取同一條數據,前後兩次讀的數據可能會出現不一致,因為可能這期間另外一個事務修改了該記錄,並提交了事務。
- 「可重複讀」隔離級別是啟動事務時生成一個 Read View,然後整個事務期間都在用這個 Read View,這樣就保證了在事務期間讀到的數據都是事務啟動前的記錄。