資料庫事務指的是一組資料操作,事務內的操作要麼就是全部成功,要麼就是全部失敗,什麼都不做,其實不是沒做,是可能做了一部分但是隻要有一步失敗,就要回滾所有操作,有點一不做二不休的意思。
在 MySQL 中,事務支援是在引擎層實現的。MySQL 是一個支援多引擎的系統,但並不是所有的引擎都支援事務。比如 MySQL 原生的 MyISAM 引擎就不支援事務,這也是 MyISAM 被 InnoDB 取代的重要原因之一。
1.1 四大特性
1.2 隔離級別
SQL 事務的四大特性中原子性、一致性、永續性都比較好理解。但事務的隔離級別確實比較難的,今天主要聊聊 MySQL 事務的隔離性。
SQL 標準的事務隔離從低到高階別依次是:讀未提交(read uncommitted)、讀提交(read committed)、可重複讀(repeatable read)和序列化(serializable )。級別越高,效率越低。
1.3 解決的並行問題
SQL 事務隔離級別的設計就是為了能最大限度的解決並行問題:
SQL 不同的事務隔離級別能解決的並行問題也不一樣,如下表所示:只有序列化的隔離級別解決了全部這 3 個問題,其他的 3 個隔離級別都有缺陷。
事務隔離級別 | 髒讀 | 不可重複讀 | 幻讀 |
---|---|---|---|
讀未提交 | 可能 | 可能 | 可能 |
讀已提交 | 不可能 | 可能 | 可能 |
可重複讀 | 不可能 | 不可能 | 可能 |
序列化 | 不可能 | 不可能 | 不可能 |
PS:不可重複讀的和幻讀很容易混淆,不可重複讀側重於修改,幻讀側重於新增或刪除。解決不可重複讀的問題只需鎖住滿足條件的行,解決幻讀需要鎖表
1.4 舉個栗子
這麼說可能有點難以理解,舉個栗子。還是之前的表結構以及表資料
CREATE TABLE `student` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `age` int(11) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 66 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
假設現在,我要同時啟動兩個食物,一個事務 A 查詢 id = 2 的學生的 age,一個事務 B 更新 id = 2 的學生的 age。流程如下,在四種隔離級別下的 X1、X2、X3 的值分別是怎樣的呢?
那為什麼會出現這樣的結果呢?事務隔離級別到底是怎麼實現的呢?
事務隔離級別是怎麼是實現的呢?我在極客時間丁奇老師的課上找到了答案:
實際上,資料庫裡面會建立一個檢視,存取的時候以檢視的邏輯結果為準。在 「可重複讀」 隔離級別下,這個檢視是在事務啟動時建立的,整個事務存在期間都用這個檢視。在 「讀提交」 隔離級別下,這個檢視是在每個 SQL 語句開始執行的時候建立的。這裡需要注意的是,「讀未提交」 隔離級別下直接返回記錄上的最新值,沒有檢視概念;而 「序列化」 隔離級別下直接用加鎖的方式來避免並行存取。
1.5 設定事務隔離級別
不同的資料庫預設設定的事務隔離級別也大不一樣,Oracle 資料庫的預設隔離級別是讀提交,而 MySQL 是可重複讀。所以,當你的系統需要把資料庫從 Oracle 遷移到 MySQL 時,請把級別設定成與搬遷之前的(讀提交)一致,避免出現不可預測的問題。
1.5.1 檢視事務隔離級別
# 檢視事務隔離級別 5.7.20 之前 SELECT @@transaction_isolation show variables like 'transaction_isolation'; # 5.7.20 以及之後 SELECT @@tx_isolation show variables like 'tx_isolation' +---------------+-----------------+ | Variable_name | Value | +---------------+-----------------+ | tx_isolation | REPEATABLE-READ | +---------------+-----------------+
1.5.2 設定隔離級別
修改隔離級別語句格式是:set [作用域] transaction isolation level [事務隔離級別]
其中作用域可選:SESSION(對談)、GLOBAL(全域性);隔離級別就是上面提到的 4 種,不區分大小寫。
例如:設定全域性隔離級別為讀提交
set global transaction isolation level read committed;
1.6 事務的啟動
MySQL 的事務啟動有以下幾種方式:
# 更新學生名字 START TRANSACTION; update student set name = '張三' where id = 2; commit;
理解了隔離級別,那事務的隔離是怎麼實現的呢?要想理解事務隔離,先得了解 MVCC 多版本的並行控制這個概念。而 MVCC 又依賴於 undo log 和 read view 實現。
2.1 什麼是 MVCC?
百度上的解釋是這樣的:
MVCC,全稱 Multi-Version Concurrency Control,即多版本並行控制。MVCC 是一種並行控制的方法,一般在資料庫管理系統中,實現對資料庫的並行存取,在程式語言中實現事務記憶體。
MVCC 使得資料庫讀不會對資料加鎖,普通的 SELECT 請求不會加鎖,提高了資料庫的並行處理能力;資料庫寫才會加鎖。 藉助 MVCC,資料庫可以實現 READ COMMITTED,REPEATABLE READ 等隔離級別,使用者可以檢視當前資料的前一個或者前幾個歷史版本,保證了 ACID 中的 I 特性(隔離性)。
MVCC 只在 REPEATABLE READ 和 READ COMMITIED 兩個隔離級別下工作。其他兩個隔離級別都和 MVCC 不相容 ,因為 READ UNCOMMITIED 總是讀取最新的資料行,而不是符合當前事務版本的資料行。而 SERIALIZABLE 則會對所有讀取的行都加鎖。
2.1.1 InnDB 中的 MVCC
InnDB 中每個事務都有一個唯一的事務 ID,記為 transaction_id。它在事務開始時向 InnDB 申請,按照時間先後嚴格遞增。
而每行資料其實都有多個版本,這就依賴 undo log 來實現了。每次事務更新資料就會生成一個新的資料版本,並把 transaction_id 記為 row trx_id。同時舊的資料版本會保留在 undo log 中,而且新的版本會記錄舊版本的回滾指標,通過它直接拿到上一個版本。
所以,InnDB 中的 MVCC 其實是通過在每行記錄後面儲存兩個隱藏的列來實現的。一列是事務 ID:trx_id;另一列是回滾指標:roll_pt。
2.2 undo log
回滾紀錄檔儲存了事務發生之前的資料的一個版本,可以用於回滾,同時可以提供多版本並行控制下的讀(MVCC),也即非鎖定讀。
根據操作的不同,undo log 分為兩種: insert undo log 和 update undo log。
2.2.1 insert undo log
insert 操作產生的 undo log,因為 insert 操作記錄沒有歷史版本只對當前事務本身可見,對於其他事務此記錄不可見,所以 insert undo log 可以在事務提交後直接刪除而不需要進行 purge 操作。
purge 的主要任務是將資料庫中已經 mark del 的資料刪除,另外也會批次回收 undo pages
所以,插入資料時。它的初始狀態是這樣的:
2.2.2 update undo log
UPDATE 和 DELETE 操作產生的 Undo log 都屬於同一型別:update_undo。(update 可以視為 insert 新資料到原位置,delete 舊資料,undo log 暫時保留舊資料)。
事務提交時放到 history list 上,沒有事務要用到這些回滾紀錄檔,即系統中沒有比這個回滾紀錄檔更早的版本時,purge 執行緒將進行最後的刪除操作。
一個事務修改當前資料:
另一個事務修改資料:
這樣的同一條記錄在資料庫中存在多個版本,就是上面提到的多版本並行控制 MVCC。
另外,藉助 undo log 通過回滾可以回到上一個版本狀態。比如要回到 V1 只需要順序執行兩次回滾即可。
2.3 read-view
read view 是 InnDB 在實現 MVCC 時用到的一致性讀檢視,用於支援 RC(讀提交)以及 RR(可重複讀)隔離級別的實現。
read view 不是真實存在的,只是一個概念,undo log 才是它的體現。它主要是通過版本和 undolog 計算出來的。作用是決定事務能看到哪些資料。
每個事務或者語句有自己的一致性檢視。普通查詢語句是一致性讀,一致性讀會根據 row trx_id 和一致性檢視確定資料版本的可見性。
2.3.1 資料版本的可見性規則
read view 中主要包含當前系統中還有哪些活躍的讀寫事務,在實現上 InnDB 為每個事務構造了一個陣列,用來儲存這個事務啟動瞬間,當前正活躍(還未提交)的事務。
前面說了事務 ID 隨時間嚴格遞增的,把系統中已提交的事務 ID 的最大值記為陣列的低水位,已建立過的事務 ID + 1記為高水位。
這個檢視陣列和高水位就組成了當前事務的一致性檢視(read view)
這個陣列畫個圖,長這樣:
規則如下:
3 如果在綠色區域,就會有兩種情況:
第三點我在看教學的時候也有點疑惑,好在有熱心網友解答:
落在綠色區域意味著是事務 ID 在低水位和高水位這個範圍裡面,而真正是否可見,看綠色區域是否有這個值。如果綠色區域沒有這個事務 ID,則可見,如果有,則不可見。在這個範圍裡面並不意味著這個範圍就有這個值,比如 [1,2,3,5],4 在這個陣列 1-5 的範圍裡,卻沒在這個陣列裡面。
這樣說可能有點難以理解,我假設一個場景:三個事務對同一條資料進行查詢更新等操作,為此畫了張圖以方便理解:
原始資料還是下圖這樣的,對 id = 2 的張三進行資訊的更新:
針對上圖,我想提個問題。分別在 RC(讀提交)以及 RR(可重複讀)隔離級別下,T4 和 T5 時間點的查詢 age 值分別是多少呢?T4 更新的值又是多少呢?思考片刻,相信大家都有自己的答案。答案在文末,希望大家能帶著自己的疑問繼續讀下去。
2.3.2 RR(可重複讀)下的結果
RR 級別下,查詢只承認在事務啟動前就已經提交完成的資料,一旦啟動事務就會建檢視。所以使用 start transaction with consistent snapshot 命令,馬上就會建檢視。
現在假設:
在這種隔離級別下,他們建立檢視的時刻如下:
根據上圖得,事務 A 的檢視陣列是[2,3];事務 B 的檢視陣列是 [2,3,4];事務 C 的檢視陣列是[2,3,4,5]。分析一波:
T4 時刻,B 讀資料都是從當前版本讀起,過程是這樣的:
T5 時刻,A 讀資料都是從當前版本讀起,過程是這樣的:
這樣執行下來,雖然期間這一行資料被修改過,但是事務 A 不論在什麼時候查詢,看到這行資料的結果都是一致的,所以我們稱之為一致性讀。
其實檢視是否可見主要看建立檢視和提交的時機,總結下規律:
事務 B 的 update 語句,如果按照上圖的一致性讀,好像結果不大對?
如下圖周明,B 的檢視陣列是先生成的,之後事務 C 才提交。那就應該看不見 C 修改的 age = 23 呀?最後 B 怎麼得出 24 了?
沒錯,如果 B 在更新之前執行查詢語句,那返回的結果肯定是 age = 22。問題是更新就不能在歷史版本更新了呀,否則 C 的更新不就丟失了?
所以,更新有個規則:更新資料都是先讀後寫(讀是更新語句執行,不是我們手動執行),讀的就是當前版本的值,叫當前讀;而我們普通的查詢語句就叫快照讀。
因此,在更新時,當前讀讀到的是 age = 23,更新之後就成 24 啦。
除了更新語句,查詢語句如果加鎖也是當前讀。如果把事務 A 的查詢語句 select age from t where id = 2 改一下,加上鎖(lock in mode 或者 for update),也都可以得到當前版本 4 返回的 age = 24
下面就是加了鎖的 select 語句:
select age from t where id = 2 lock in mode; select age from t where id = 2 for update;
假設事務 C 不馬上提交,但是 age = 23 版本已生成。事務 B 的更新將會怎麼走呢?
事務 C 還沒提交,寫鎖還沒釋放,但是事務 B 的更新必須要當前讀且必須加鎖。所以事務 B 就阻塞了,必須等到事務 C 提交,釋放鎖才能繼續當前的讀。
2.3.3 RC(讀提交)下的結果
在讀提交隔離級別下,查詢只承認在語句啟動前就已經提交完成的資料;每一個語句執行之前都會重新算出一個新的檢視。
注意:在上圖的表格中用於啟動事務的是 start transaction with consistent snapshot 命令,它會建立一個持續整個事務的檢視。所以,在 RC 級別下,這命令其實不起作用。等效於普通的 start transaction(在執行 sql 語句之前才算是啟動了事務)。所以,事務 B 的更新其實是在事務 C 之後的,它還沒真正啟動事務,而 C 已提交。
現在假設:
在這種隔離級別下,他們建立檢視的時刻如下:
根據上圖得,事務 A 的檢視陣列是[2,3,4],但它的高水位是 6或者更大(已建立事務 ID + 1);事務 B 的檢視陣列是 [2,4];事務 C 的檢視陣列是 [2,5]。分析一波:
T4 時刻,B 讀資料都是從當前版本讀起,過程是這樣的:
T5 時刻,A 讀資料都是從當前版本讀起,過程是這樣的:
本文詳細聊了事務的方方面面,比如:四大特性、隔離級別、解決的並行問題、如何設定、檢視隔離級別、如何啟動事務等。除此以外,還深入瞭解了 RR 和 RC 兩個級別的隔離是怎麼實現的?包括詳解 MVCC、undo log 和 read view 是怎麼配合實現 MVCC 的。最後還聊了快照讀、當前讀等等。可以說,事務相關的知識點都在這了。看完這一篇還不懂的話,你來捶我呀!
【相關推薦:】
以上就是一文詳解MySQL中的事務和 MVCC 原理的詳細內容,更多請關注TW511.COM其它相關文章!