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

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

  • 原子性(atomicity)
  • 一致性(consistency)
  • 隔離性(isolation)
  • 持久性(durability)
  • 隔離性

隔離性的實現原理就是鎖,於是隔離性也能夠稱爲併發控制、鎖等。事務的隔離性要求每一個讀寫事務的對象對其餘事務的操做對象能互相分離。mysql

再者,好比操做緩衝池中的 LRU 列表,刪除,添加、移動 LRU 列表中的元素,爲了保證一致性那麼就要鎖的介入。web

InnoDB 使用鎖爲了支持對共享資源進行併發訪問,提供數據的完整性和一致性。 算法

那麼到底 InnoDB 支持什麼樣的鎖呢?咱們先來看下 InnoDB 的鎖的介紹:sql

  • 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 實現的。

畫張圖你就明白了:

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 B insert 一條數據,超時:

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(可重複讀)(mysql默認隔離級別)

 

一個事務按相同的查詢條件讀取之前檢索過的數據,其餘事務插入了知足其查詢條件的新數據,產生幻讀。

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 須要持久性保護 。

 

 轉自:https://mp.weixin.qq.com/s/j7rf0kE9-Z80OsUJymoPBw

相關文章
相關標籤/搜索