Mysql 事務及其原理
什么是事務
什么是事務?事務是作為單個邏輯工作單元執行的一系列操作,通俗易懂的說就是一組原子性的 SQL 查詢。Mysql 中事務的支持在存儲引擎層,MyISAM 存儲引擎不支持事務,而 InnoDB 支持,這是 Mysql 5.5.5 以后默認引擎由 MyISAM 換成 InnoDB 的最根本原因。
事務的 ACID 屬性
原子性(Atomicity):作為邏輯工作單元,一個事務里的所有操作的執行,要么全部成功,要么全部失敗。
一致性(Consistency):數據庫從一個一致性狀態變換到另外一個一致性狀態,數據庫的完整性不會受到破壞。
隔離性(Isolation):通常來說,一個事務所做的修改在最終提交前,對其他事務是不可見的。為什么是通常來說,為了提高事務的並發引出不同的隔離級別,具體參考下一章節。
持久性(Durability):一旦事務提交,則其所做的修改就會永久保存到數據庫中,即使系統故障,修改的數據也不會丟失。
事務的隔離級別
為了盡可能的高並發,事務的隔離性被分為四個級別:讀未提交、讀已提交、可重復讀和串行化。用戶可以根據需要選擇不同的級別。
未提交讀(READ UNCOMMITTED):一個事務還未提交,它的變更就能被別的事務看到。
例:事務 A 可以讀到事務 B 修改的但還未提交的數據,會導致臟讀(可能事務 B 在提交后失敗了,事務 A 讀到的數據是臟的)。
提交讀(READ COMMITTED):一個事務提交后,它的變更才能被其他事務看到。大多數據庫系統的默認級別,但 Mysql 不是。
例:事務 A 只能讀到事務 B 修改並提交后的數據,會導致不可重復讀(事務 A 中執行兩次查詢,一次在事務 B 提交過程中,一次在事務 B 提交之后,會導致兩次讀取的結果不一致)。
可重復讀(REPEATABLE READ):未提交的事務的變更不能被其他事務看到,同時一次事務過程中多次讀取同樣記錄的結果是一致的。 例:事務 A 在執行過程中多次獲取某范圍內的記錄,事務 B 提交后在此范圍內插入或者刪除 N條記錄,事務 A 執行過程中多次范圍讀會存在不一致,即幻讀(Mysql 的默認級別,InnoDB 通過 MVVC 解決了幻讀的問題)。
可串行化(SERIALIZABLE):當兩個事務間存在讀寫沖突時,數據庫通過加鎖強制事務串行執行,解決了前面所說的所有問題(臟讀、不可重復讀、幻讀)。是最高隔離的隔離級別。
用表格可以更清晰的描述四種隔離級別的定義和可能存在的問題:
Mysql 中的事務
1、事務的自動提交
Mysql 默認采用自動提交(AUTOCOMMIT)模式,也就是說,如果不顯示地開始一個事務,則每個查詢都被當做一個事務執行提交操作。 可以通過以下命令查看 mysql 是否打開自動提交,
mysql> show variables like 'AUTOCOMMIT';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.01 sec)
--1 或者 ON 表示啟用, 0 或者 OFF 表示禁用
mysql> SET AUTOCOMMIT = 0/1;
--以上命令可以打開和關閉自動提交
1、通過 set autocommit = 0 關閉當前會話的自動提交,如果需要對全局生效必須再配置文件中進行修改。
2、關閉自動提交后,用戶的所有 DML 語句都會在同一個事務中,直到遇到 COMMIT 或 ROLLBACK 指令結束事務。
一張動圖來說明以上兩點:
當然,用戶可以通過 start transaction 或者 begin 顯示的開啟一個事務。**顯示的開啟事務會自動執行 set autocommit = 0,並在 commit 或 rollback 結束一個事務后執行 set autocommit = 1。**更多事務的控制語句如下:
START TRANSACTION | BEGIN: 顯式地開啟一個事務;
COMMIT:也可以使用 COMMIT WORK,不過二者是等價的。COMMIT 會提交事務,並使已對數據庫進行的所有修改成為永久性的;
ROLLBACK:也可以使用 ROLLBACK WORK,不過二者是等價的。回滾會結束用戶的事務,並撤銷正在進行的所有未提交的修改;
SAVEPOINT identifier:SAVEPOINT 允許在事務中創建一個保存點,一個事務中可以有多個 SAVEPOINT;
RELEASE SAVEPOINT identifier:刪除一個事務的保存點,當沒有指定的保存點時,執行該語句會拋出一個異常;
ROLLBACK TO identifier:把事務回滾到標記點;
SET TRANSACTION:用來設置事務的隔離級別。
2、事務的隔離級別
Mysql 支持事務最流行的存儲引擎非 InnoDB 莫屬,所以以下的 Mysql 隔離級別設置都是基於 InnoDB 的。
1、查看InnoDB存儲引擎系統級的隔離級別和會話級的隔離級別,命令和結果如下:
mysql> select @@global.tx_isolation,@@tx_isolation;
+-----------------------+-----------------+
| @@global.tx_isolation | @@tx_isolation |
+-----------------------+-----------------+
| REPEATABLE-READ | REPEATABLE-READ |
+-----------------------+-----------------+
2、設置InnoDB存儲引擎隔離級別:
語句:
set [ global | session ] transaction isolation level Read uncommitted | Read committed | Repeatable | Serializable;
示例:
mysql> set session transaction isolation level Serializable;
Query OK, 0 rows affected (0.01 sec)
mysql> select @@global.tx_isolation,@@tx_isolation;
+-----------------------+----------------+
| @@global.tx_isolation | @@tx_isolation |
+-----------------------+----------------+
| REPEATABLE-READ | SERIALIZABLE |
+-----------------------+----------------+
1 row in set (0.00 sec)
3、事務的 MVCC 機制
Mysql 的事務型存儲引擎(InnoDB)使用 MVCC(Multi-Version Concurrency Control,多版本並發控制)代替行級鎖來提高並發讀寫的性能。InnoDB 的 MVCC 原理比較簡單,它通過在在每行記錄后面保存三個隱藏列(事務 id,行的創建的版本號、行的過期版本號)來實現的,下面是 InnoDB 在 REPEATABLE READ 隔離級別下 MVCC 的簡化工作原理:
INSERT: InnoDB 為新插入的每一行保存當前系統版本號作為行版本號。
UPDATE: InnoDB為插入一行新記錄,保存當前系統版本號作為行版本號,同時保存當前系統版本號到原來的行作為行刪除標識。
DELETE: InnoDB為刪除的每一行保存當前系統版本號作為行刪除標識。
SELECT: InnoDB會根據以下兩個條件檢查每行記錄:
- InnoDB只查找版本早於當前事務版本的數據行(也就是,行的系統版本號小於或等於事務的系統版本號),這樣可以確保事務讀取的行,要么是在事務開始前已經存在的,要么是事務自身插入或者修改過的。
- 行的刪除版本要么未定義,要么大於當前事務版本號。這可以確保事務讀取到的行,在事務開始之前未被刪除。
只有符合上述兩個條件的記錄,才能返回作為查詢結果。
下面用更淺顯易懂的例子說明 MVCC 下的 INSERT/DELETE/UPDATE/SELECT 操作: 假如 test 表有兩個字段 name 和 age;MVCC 的三個隱藏列字段名為 transaction_id、 create_version 和 delete_version。
insert
- delete_version 未定義或者大於 select 所在事務的 delete_version 的行。
- create_version 小於或等於 select 所在事務的的 create_version。
通過這個例來看下為什么 MVCC 在 REPEATABLE READ 隔離級別下能解決幻讀。假如有個事務開始於 update 之后 delete 之前,且結束於 delete 之后,如下:
start transaction; //假如事務 id = 2.5
select * from test; //執行時間在 update 之后 delete 之前 select * from test; //執行時間在 delete 之后 commit;
如果不使用 MVCC 第一條 select * from test 能讀到 1 條記錄,而 第二條將讀取到 0 條記錄,同一事務中多次 select 范圍查詢讀取到的記錄不一致即幻讀。而使用 MVVC 之后,兩條 select 語句讀取到的記錄相同。
MVCC 只在 REPEATABLE READ 和 READ COMMITTED 兩個隔離級別下工作。其他兩個隔離級別都和MVCC不兼容,因為 READ UNCOMMITTED 總是讀取最新的數據行,而不是符合當前事務版本的數據行。而 SERIALIZABLE 則會對所有讀取的行都加鎖。
4、事務的實現(redo/undo log)
事務的隔離性通過鎖或 MVCC 機制來實現,而原子性、持久性和一致性通過 redo/undo log 來完成。redo log 稱為重做日志,用來保證事務的原子性和持久性。undo log 稱為撤銷日志,用來保證事務的一致性。
1、redo log
基本概念
重做日志用來實現事務的持久性,由以下兩部分組成:
- 重做日志緩沖區(redo log buffer),內存中,易丟失。
- 重做日志文件(redo log file),磁盤中,持久的。
redo log file 是順序寫入的,在數據庫運行時不需要進行讀取,只會在數據庫啟動的時候讀取來進行數據的恢復工作。 redo log file 是物理日志,所謂的物理日志是指日志中的內容都是直接操作物理頁的命令。重做時是對某個物理頁進行相應的操作。
整體流程
更新事務操作一次數據的流程圖如下所示:
-
第一步:先將原始數據從磁盤中讀入內存中來,修改數據的內存拷貝。
-
第二步:生成一條重做日志並寫入redo log buffer,記錄的是數據被修改后的值。
-
第三步:在必要的時候,采用追加寫的方式將 redo log buffer 中的內容刷新到 redo log file。
-
第四步:定期將內存中修改的數據刷新到磁盤中。
以上比較重要的是第三步,其中必要的時候有以下幾種情況:
- 事務提交時(最常見的情景,在 commit 之前)
- 當 log buffer 中有一半的內存空間被使用時
- log checkpoint 時
- 實例 shutdown 時
- binlog切換時
- 后台線程
寫入策略
學過 linux 操作系統的都知道內存中數據寫入到磁盤文件中時如果不打開 O_DIRECT 選項。數據是要先寫入文件操作系統緩存區中的,然后再某個時刻 flush 到磁盤。流程如下:
事務提交時將 redo log buffer 寫入 redo log file,為了保證數據一定能正確同步到磁盤(不僅僅只寫到文件緩沖區中)文件中,InndoDB 默認情況下調用了 fsync 進行寫操作。而 fsync 的性能比較低。當然這只是默認情況,InnoDB 也提供了參數 innodb_flush_log_at_trx_commit 來配置 redo log 刷新到磁盤的策略,有以下三個值:
- 當設置該值為 1 時,每次事務提交都要做一次 fsync,這是最安全的配置,即使宕機也不會丟失事務;
- 當設置為 2 時,則在事務提交時只做 write 操作,只保證寫到系統的緩沖區,因此實例crash不會丟失事務,但宕機則可能丟失事務;
- 當設置為 0 時,事務提交不會觸發 redo 寫操作,而是留給后台線程每秒一次的刷盤操作,因此實例 crash 將最多丟失一秒鍾內的事務。
用下圖可以更直觀的說明 innodb_flush_log_at_trx_commit 不同值下的不同策略。操作越接近磁盤性能越低,當然可靠性越來越高。故性能:1 < 2 < 0,可靠性:0 < 2 < 1。
當 innodb_flush_log_at_trx_commit 設置為 0 或者 2 時喪失了事務的 ACID 特性,通常在日常環境時將其設置為 1,而在系統高峰時將其設置為 2 以應對大負載。
恢復
InnoDB 引擎啟動時不管上次數據庫運行時是否正常關閉,都會嘗試進行恢復操作。整個恢復操作有如下特點:
- 從 checkpoint 開始的日志部分進行恢復。
- 順序讀取及並行應用重做日志。
- 重做日志的應用具有冪等性。
- 重做日志是物理日志,恢復的速度相對較快。
2、undo log
基本概念
- undo log 用來實現事務的一致性,InndoDB 可以通過 redo log 對頁進行重做操作。但是有時候事務需要進行回滾,這時就需要 undo log。
- undo log 還可可以用來協助 InnoDB 引擎實現 MVCC 機制。
- undo log 是邏輯日志,恢復時並不是對物理頁直接進行恢復,而是邏輯地將數據庫恢復到原來的樣子。
- undo log 的產生也會伴隨着 redo log 的產生。
寫入時機
事務開始之前,流程如圖:
日志格式
在 InnoDB 中 undo log 分為 insert undo log 和 update undo log
insert undo log
insert 操作產生的日志。根據隔離性,insert 插入的記錄只對本事務可見,所以事務提交后可以刪除因 insert 產生的日志。
update undo log
delete 和 update 操作產生的日志。根據前面的 MVCC 機制可以知道此部分記錄還有可能要被其他事務所使用,所以即使事務提交也不能刪除相應的日志。在事務提交時會被保存到 undo log 鏈表,在 purge 線程中做最后的刪除。
3、redo 與 undo log 記錄過程
undo 記錄更新之前的日志,為了回滾。 redo 記錄更新之后的日志,為了重做。 redo log 與 undo log 產生過程的簡化版本如下,可以更方便的理解 redo 與 undo 的區別。
假設有A,B兩個數據,原值分別為 1, 2;現將A更新為10,B 更新為 20;undo log記錄信息過程如下: 1. 事務開始 2. 記錄 A = 1 到 undo log 3. 更新 A = 10 4. 記錄 A = 10 到 redo log 5. 記錄 B = 2 到 undo log 6. 更新 B = 20 7. 記錄 B = 20 到 redo log 8. redo log 信息寫入磁盤 9. 提交事務
