在 MySQL 中,事務支持是在引擎層實現的。MySQL 是一個支持多引擎的系統,但并不是所有的引擎都支持事務。比如 MySQL 原生的 MyISAM 引擎就不支持事務,這也是 MyISAM 被 InnoDB 取代的重要原因之一。
1.1 四大特性
原子性(Atomicity):事務開始后所有操作,要么全部做完,要么全部不做,不可能停滯在中間環節。事務執行過程中出錯,會回滾到事務開始前的狀態,所有的操作就像沒有發生一樣。也就是說事務是一個不可分割的整體,就像化學中學過的原子,是物質構成的基本單位。 一致性(Consistency):事務開始前和結束后,數據庫的完整性約束沒有被破壞 。比如 A 向 B 轉賬,不可能 A 扣了錢,B 卻沒收到。 隔離性(Isolation):同一時間,只允許一個事務請求同一數據,不同的事務之間彼此沒有任何干擾。比如 A 正在從一張銀行卡中取錢,在 A 取錢的過程結束前,B 不能向這張卡轉賬。 持久性(Durability):事務完成后,事務對數據庫的所有更新將被保存到數據庫,不能回滾。 1.2 隔離級別
SQL 事務的四大特性中原子性、一致性、持久性都比較好理解。但事務的隔離級別確實比較難的,今天主要聊聊 MySQL 事務的隔離性。
臟讀:事務 A 讀取了事務 B 更新的數據,然后 B 回滾操作,那么 A 讀取到的數據是臟數據 不可重復讀:事務 A 多次讀取同一數據,事務 B 在事務 A 多次讀取的過程中,對數據作了更新并提交,導致事務 A 多次讀取同一數據時,結果不一致。 幻讀:系統管理員 A 將數據庫中所有學生的成績從具體分數改為 ABCDE 等級,但是系統管理員 B 就在這個時候插入了一條具體分數的記錄,當系統管理員 A 改結束后發現還有一條記錄沒有改過來,就好像發生了幻覺一樣,這就叫幻讀。 SQL 不同的事務隔離級別能解決的并發問題也不一樣,如下表所示:只有串行化的隔離級別解決了全部這 3 個問題,其他的 3 個隔離級別都有缺陷。
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 的值分別是怎樣的呢?
讀未提交:X1 的值是 23,因為事務 B 雖然沒提交但它的更改已被 A 看到。(如果 B 后面又回滾了 X1 的值就是臟的)。X2、X3 的值也是 23,這無可厚非。 讀已提交:X1 的值是 22,因為 B 雖然改了,但 A 看不到。(如果 B 后面回滾了,X1 的值不變,解決了臟讀),X2、X3 的值是 23,沒毛病,B 提交了,A 才能看到。 可重復讀:X1、X2 都是 22,A 開啟的時刻值是 22,那么在 A 的整個過程中,它的值都是 22。(不管 B 在這期間怎么修改,只要 A 還沒提交,都是看不見的,解決了不可重復讀),而 X3 的值是 23,因為 A 提交了,能看到 B 修改的值了。 串行化:B 在執行更改期間會被鎖住,直至 A 提交。B 才能繼續執行。(A 在讀期間,B 不能寫。得保證此時數據是最新的。解決了幻讀)所以 X1、X2 都是 22,而最后的 X3 在 B 提交之后執行,它的值就是 23。 那為什么會出現這樣的結果呢?事務隔離級別到底是怎么實現的呢?
2.3.2.2 select 當前讀 除了更新語句,查詢語句如果加鎖也是當前讀。如果把事務 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; 2.3.2.3 事務 C 不馬上提交 假設事務 C 不馬上提交,但是 age = 23 版本已生成。事務 B 的更新將會怎么走呢?
事務 C 還沒提交,寫鎖還沒釋放,但是事務 B 的更新必須要當前讀且必須加鎖。所以事務 B 就阻塞了,必須等到事務 C 提交,釋放鎖才能繼續當前的讀。
注意:在上圖的表格中用于啟動事務的是 start transaction with consistent snapshot 命令,它會創建一個持續整個事務的視圖。所以,在 RC 級別下,這命令其實不起作用。等效于普通的 start transaction(在執行 sql 語句之前才算是啟動了事務)。所以,事務 B 的更新其實是在事務 C 之后的,它還沒真正啟動事務,而 C 已提交。
現在假設:
事務 A 開始前,只有一個活躍的事務,ID = 2, 已提交的事務也就是插入數據的事務 ID = 1 事務 A、B、C 的事務 ID 分別是 3、4、5 在這種隔離級別下,他們創建視圖的時刻如下:
根據上圖得,事務 A 的視圖數組是[2,3,4],但它的高水位是 6或者更大(已創建事務 ID + 1);事務 B 的視圖數組是 [2,4];事務 C 的視圖數組是 [2,5]。分析一波: