1. 程式人生 > >一文快速搞懂MySQL InnoDB事務ACID實現原理

一文快速搞懂MySQL InnoDB事務ACID實現原理

test 用戶 bin 輔助索引 做的 text 訪問 通過 可重復

【51CTO.com原創稿件】說到數據庫事務,想到的就是要麽都做修改,要麽都不做,或者是 ACID 的概念。其實事務的本質就是鎖、並發和重做日誌的結合體。技術分享圖片

這一篇主要講一下 InnoDB 中的事務到底是如何實現 ACID 的:

  • 原子性(atomicity)
  • 一致性(consistency)
  • 隔離性(isolation)
  • 持久性(durability)
    隔離性
    隔離性的實現原理就是鎖,因而隔離性也可以稱為並發控制、鎖等。事務的隔離性要求每個讀寫事務的對象對其他事務的操作對象能互相分離。
    再者,比如操作緩沖池中的 LRU 列表,刪除,添加、移動 LRU 列表中的元素,為了保證一致性那麽就要鎖的介入。
    InnoDB 使用鎖為了支持對共享資源進行並發訪問,提供數據的完整性和一致性。
    那麽到底 InnoDB 支持什麽樣的鎖呢?我們先來看下 InnoDB 的鎖的介紹:
    InnoDB 中的鎖
    你可能聽過各種各樣的 InnoDB 的數據庫鎖,Gap 鎖,共享鎖,排它鎖,讀鎖,寫鎖等等。但是 InnoDB 的標準實現的鎖只有 2 類,一種是行級鎖,一種是意向鎖。
    InnoDB 實現了如下兩種標準的行級鎖:
  • 共享鎖(讀鎖 S Lock),允許事務讀一行數據。
  • 排它鎖(寫鎖 X Lock),允許事務刪除一行數據或者更新一行數據。
    行級鎖中,除了 S 和 S 兼容,其他都不兼容。
    InnoDB 支持兩種意向鎖(即為表級別的鎖):
  • 意向共享鎖(讀鎖 IS Lock),事務想要獲取一張表的幾行數據的共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的 IS 鎖。
  • 意向排他鎖(寫鎖 IX Lock),事務想要獲取一張表中幾行數據的排它鎖,事務在給一個數據行加排它鎖前必須先取得該表的 IX 鎖。
    首先解釋一下意向鎖,以下為意向鎖的意圖解釋:
    The main purpose of IX and IS locks is to show that someone is locking a row, or going to lock a row in the table.
    大致意思是加意向鎖為了表明某個事務正在鎖定一行或者將要鎖定一行數據。
    首先申請意向鎖的動作是 InnoDB 完成的,怎麽理解意向鎖呢?例如:事務 A 要對一行記錄 R 進行上 X 鎖,那麽 InnoDB 會先申請表的 IX 鎖,再鎖定記錄 R 的 X 鎖。
    在事務 A 完成之前,事務 B 想要來個全表操作,此時直接在表級別的 IX 就告訴事務 B 需要等待而不需要在表上判斷每一行是否有鎖。
    意向排它鎖存在的價值在於節約 InnoDB 對於鎖的定位和處理性能。另外註意了,除了全表掃描以外意向鎖都不會阻塞。
    鎖的算法
    InnoDB 有 3 種行鎖的算法:
  • Record Lock:單個行記錄上的鎖。
  • Gap Lock:間隙鎖,鎖定一個範圍,而非記錄本身。
  • Next-Key Lock:結合 Gap Lock 和 Record Lock,鎖定一個範圍,並且鎖定記錄本身。主要解決的問題是 RR 隔離級別下的幻讀。
    這裏主要講一下 Next-Key Lock。MySQL 默認隔離級別 RR 下,這時默認采用 Next-Key locks。
    這種間隙鎖的目的就是為了阻止多個事務將記錄插入到同一範圍內從而導致幻讀。註意了,如果走唯一索引,那麽 Next-Key Lock 會降級為 Record Lock。
    前置條件為事務隔離級別為 RR 且 SQL 走的非唯一索引、主鍵索引。如果不是則根本不會有 Gap 鎖!先舉個例子來講一下 Next-Key Lock。
    首先建立一張表:
    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 |
+----+---------+-------+

`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 實現的。
畫張圖你就明白了:![](https://s1.51cto.com/images/blog/201904/03/ea21d2246fdb9b22d050882a0d885d2a.jpg?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

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 加鎖的順序不一致。
不懂死鎖概念模型的可以先看一幅圖:![](https://s1.51cto.com/images/blog/201904/03/e18abb279d6c1c7494a70460f0ba8c52.jpg?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

左鳥線程獲取了左肉的鎖,想要獲取右肉的鎖,右鳥的線程獲取了右肉的鎖。
右鳥想要獲取左肉的鎖。左鳥沒有釋放左肉的鎖,右鳥也沒有釋放右肉的鎖,那麽這就是死鎖。
接下來還用剛才的那張 M 表來分析一下數據庫死鎖,比較好理解:![](https://s1.51cto.com/images/blog/201904/03/b9ba3e051a8796966437328ac5cd9bd5.jpg?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

**四種隔離級別**
那麽按照最嚴格到最松的順序來講一下四種隔離級別:
**①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 隔離級別下,對於快照數據,總是讀最新的一份快照數據見紅標:
![](https://s1.51cto.com/images/blog/201904/03/abed48da5c98aad28c72bb229d277d43.jpg?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
undo log 會產生 redo log,因為 undo log 需要持久性保護 。
最後,你會發現姜承堯的 MySQL InnoDB 書上的很多內容都是官方手冊的翻譯,無論是看源碼還是學習新框架,最好看原汁原味的。
只要你堅持,一步一步來,總歸會成功的。切忌,學技術急不來,快就是穩,穩就是快。
來源:https://www.cnblogs.com/GrimMjx/p/10575147.html

【51CTO原創稿件,合作站點轉載請註明原文作者和出處為51CTO.com】

一文快速搞懂MySQL InnoDB事務ACID實現原理