【51CTO.com原創稿件】說到數據庫事務,想到的就是要麼都作修改,要麼都不作,或者是 ACID 的概念。其實事務的本質就是鎖、併發和重作日誌的結合體。html
這一篇主要講一下 InnoDB 中的事務究竟是如何實現 ACID 的:mysql
mysql> show create table m_test_db.M; +-------+----------------------------------------------------------+ | Table | Create Table | +-------+----------------------------------------------------------+ | M | CREATE TABLE `M` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` varchar(45) DEFAULT NULL, `name` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`), KEY `IDX_USER_ID` (`user_id`) ) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8 | +-------+----------------------------------------------------------+ 1 row in set (0.00 sec)
首先 Session A 去拿到 user_id 爲 26 的 X 鎖,用 force index,強制走這個非惟一輔助索引,由於這張表裏的數據不多。算法
mysql> begin; Query OK, 0 rows affected (0.00 sec)
mysql> select * from m_test_db.M force index(IDX_USER_ID) where user_id = '26' for update;
+----+---------+-------+
| id | user_id | name |
+----+---------+-------+
| 5 | 26 | jerry |
| 6 | 26 | ketty |
+----+---------+-------+ sql
`2 rows in set (0.00 sec) ` 而後 Session B 插入數據:
mysql> begin;
Query OK, 0 rows affected (0.00 sec) 數據庫
mysql> insert into m_test_db.M values (8,25,'GrimMjx');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction 數據結構
明明插入的數據和鎖住的數據沒有毛線關係,爲何還會阻塞等鎖最後超時呢?這就是 Next-Key Lock 實現的。 畫張圖你就明白了: Gap 鎖鎖住的位置,不是記錄自己,而是兩條記錄之間的間隔 Gap,其實就是防止幻讀(同一事務下,連續執行兩句一樣的 SQL 獲得不一樣的結果)。 爲了保證圖上 3 個小箭頭中間不會插入知足條件的新記錄,因此用到了 Gap 鎖防止幻讀。 簡單的 Insert 會在 Insert 的行對應的索引記錄上加一個 Record Lock 鎖,並無 Gap 鎖,因此並不會阻塞其餘 Session 在 Gap 間隙裏插入記錄。 不過在 Insert 操做以前,還會加一種鎖,官方文檔稱它爲 Intention Gap Lock,也就是意向的 Gap 鎖。 這個意向 Gap 鎖的做用就是預示着當多事務併發插入相同的 Gap 空隙時,只要插入的記錄不是 Gap 間隙中的相同位置,則無需等待其餘 Session 就可完成,這樣就使得 Insert 操做無須加真正的 Gap Lock。 Session A 插入數據: mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> insert into m_test_db.M values (10,25,'GrimMjx');Query OK, 1 row affected (0.00 sec) Session B 插入數據,徹底沒有問題,沒有阻塞: mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> insert into m_test_db.M values (11,27,'Mjx');Query OK, 1 row affected (0.00 sec) **死鎖** 瞭解了 InnoDB 是如何加鎖的,如今能夠去嘗試分析死鎖。死鎖的本質就是兩個事務相互等待對方釋放持有的鎖致使的,關鍵在於不一樣 Session 加鎖的順序不一致。 不懂死鎖概念模型的能夠先看一幅圖: 左鳥線程獲取了左肉的鎖,想要獲取右肉的鎖,右鳥的線程獲取了右肉的鎖。 右鳥想要獲取左肉的鎖。左鳥沒有釋放左肉的鎖,右鳥也沒有釋放右肉的鎖,那麼這就是死鎖。 接下來還用剛纔的那張 M 表來分析一下數據庫死鎖,比較好理解: **四種隔離級別** 那麼按照最嚴格到最鬆的順序來說一下四種隔離級別: **①Serializable(可序列化)** 最高事務隔離級別。主要用在 InnoDB 存儲引擎的分佈式事務。強制事務排序,串行化執行事務。 不須要衝突控制,可是慢速設備。根據 Jim Gray 在《Transaction Processing》一書中指出,Read Committed 和 Serializable 的開銷幾乎是同樣的,甚至 Serializable 更優。 Session A 設置隔離級別爲 Serializable,並開始事務執行一句 SQL: mysql> select @@tx_isolation; +----------------+ | @@tx_isolation | +----------------+ | SERIALIZABLE | +----------------+ 1 row in set, 1 warning (0.00 sec) mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from m_test_db.M; +----+---------+-------+ | id | user_id | name | +----+---------+-------+ | 1 | 20 | mjx | | 2 | 21 | ben | | 3 | 23 | may | | 4 | 24 | tom | | 5 | 26 | jerry | | 6 | 26 | ketty | | 7 | 28 | kris | +----+---------+-------+ 7 rows in set (0.00 sec) Session Binsert 一條數據,超時: mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into m_test_db.M values (9,30,'test'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction **②Repeatable Read(可重複讀)** 一個事務按相同的查詢條件讀取之前檢索過的數據,其餘事務插入了知足其查詢條件的新數據,產生幻讀。 InnoDB 存儲引擎在 RR 隔離級別下,已經使用 Next-Key Lock 算法避免了幻讀,瞭解概念便可。 InnoDB 使用 MVCC 來讀取數據,RR 隔離級別下,老是讀取事務開始時的行數據版本。 Session A 查看 id=1 的數據: mysql> set tx_isolation='repeatable-read'; Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from m_test_db.M where id =1; +----+---------+---------+ | id | user_id | name | +----+---------+---------+ | 1 | 20 | GrimMjx | +----+---------+---------+ 1 row in set (0.01 sec) Session B 修改 id=1 的數據: mysql> set tx_isolation='repeatable-read'; Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> update m_test_db.M set name = 'Mjx'; Query OK, 7 rows affected (0.00 sec) Rows matched: 7 Changed: 7 Warnings: 0 而後如今 Session A 再查看一下 id=1 的數據,數據仍是事務開始時候的數據。 mysql> select * from m_test_db.M where id =1; +----+---------+---------+ | id | user_id | name | +----+---------+---------+ | 1 | 20 | GrimMjx | +----+---------+---------+ 1 row in set (0.00 sec) **③Read Committed(讀已提交)** 事務從開始直到提交以前,所作的任何修改對其餘事務都是不可見的。 InnoDB 使用 MVCC 來讀取數據,RC 隔離級別下,老是讀取被鎖定行最新的快照數據。 Session A 查看 id=1 的數據: mysql> set tx_isolation='read-committed'; Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from m_test_db.M where id =1; +----+---------+------+ | id | user_id | name | +----+---------+------+ | 1 | 20 | Mjx | +----+---------+------+ 1 row in set (0.00 sec) Session B 修改 id=1 的 Name 而且 Commit: mysql> set tx_isolation='repeatable-read'; Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> update m_test_db.M set name = 'testM' where id =1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 // 注意,這裏commit了! mysql> commit; Query OK, 0 rows affected (0.00 sec) Session A 再查詢 id=1 的記錄,發現數據已是最新的數據: mysql> select * from m_test_db.M where id =1; +----+---------+-------+ | id | user_id | name | +----+---------+-------+ | 1 | 20 | testM | +----+---------+-------+ 1 row in set (0.00 sec) **④Read Uncommitted(讀未提交)** 事務中的修改,即便沒有提交,對其餘事務也都是可見的。 Session A 查看一下 id=3 的數據,沒有 Commit: mysql> set tx_isolation='read-uncommitted'; Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> select @@tx_isolation; +------------------+ | @@tx_isolation | +------------------+ | READ-UNCOMMITTED | +------------------+ 1 row in set, 1 warning (0.00 sec) mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from m_test_db.M where id =3; +----+---------+------+ | id | user_id | name | +----+---------+------+ | 3 | 23 | may | +----+---------+------+ 1 row in set (0.00 sec) Session B 修改 id=3 的數據,可是沒有 Commit: mysql> set tx_isolation='read-uncommitted'; Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> update m_test_db.M set name = 'GRIMMJX' where id = 3; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 Session A 再次查看則看到了新的結果: mysql> select * from m_test_db.M where id =3; +----+---------+---------+ | id | user_id | name | +----+---------+---------+ | 3 | 23 | GRIMMJX | +----+---------+---------+ 1 row in set (0.00 sec) 這裏花了不少筆墨來介紹隔離性,這是比較重要,須要靜下心來學習的特性。因此也是放在第一個的緣由。 **原子性、一致性、持久性** 事務隔離性由鎖實現,原子性、一致性和持久性由數據庫的 redo log 和 undo log 實現。 redo log 稱爲重作日誌,用來保證事務的原子性和持久性,恢復提交事務修改的頁操做。 undo log 來保證事務的一致性,undo 回滾行記錄到某個特性版本及 MVCC 功能。二者內容不一樣。redo 記錄物理日誌,undo 是邏輯日誌。 **redo** 重作日誌由重作日誌緩衝(redo log buffer)和重作日誌文件(redo log file)組成,前者是易失的,後者是持久的。 InnoDB 經過 Force Log at Commit 機制來實現持久性,當 Commit 時,必須先將事務的全部日誌寫到重作日誌文件進行持久化,待 Commit 操做完成纔算完成。 當事務提交時,日誌不寫入重作日誌文件,而是等待一個事件週期後再執行 Fsync 操做,因爲並不是強制在事務提交時進行一次 Fsync 操做,顯然這能夠提升數據庫性能。 請記住 3 點: 重作日誌是在 InnoDB 層產生的。 重作日誌是物理格式日誌,記錄的是對每一個頁的修改。 重作日誌在事務進行中不斷被寫入。 **undo** 事務回滾和 MVCC,這就須要 undo。undo 是邏輯日誌,只是將數據庫邏輯恢復到原來的樣子,可是數據結構和頁自己在回滾以後可能不一樣。 例如:用戶執行 insert 10w 條數據的事務,表空間於是增大。用戶執行 ROLLBACK 以後,會對插入的數據回滾,可是表空間大小不會所以收縮。 實際的作法就是作與以前想法的操做,Insert 對應 Delete,Update 對應反向 Update 來實現原子性。 InnoDB 中 MVCC 的實現就是靠 undo,舉個經典的例子:Bob 給 Smith 轉 100 元,那麼就存在如下 3 個版本,RR 隔離級別下,對於快照數據,老是讀事務開始的行數據版本見黃標。 RC 隔離級別下,對於快照數據,老是讀最新的一份快照數據見紅標:  undo log 會產生 redo log,由於 undo log 須要持久性保護 。 最後,你會發現姜承堯的 MySQL InnoDB 書上的不少內容都是官方手冊的翻譯,不管是看源碼仍是學習新框架,最好看原汁原味的。 只要你堅持,一步一步來,總歸會成功的。切忌,學技術急不來,快就是穩,穩就是快。 來源:https://www.cnblogs.com/GrimMjx/p/10575147.html 【51CTO原創稿件,合做站點轉載請註明原文做者和出處爲51CTO.com】