搞懂MySQL InnoDB事務ACID實現原理

前言

  說到數據庫事務,想到的就是要麼都作修改,要麼都不作。或者是ACID的概念。其實事務的本質就是鎖和併發和重作日誌的結合體。那麼,這一篇主要講一下InnoDB中的事務究竟是如何實現ACID的。html

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

一.隔離性

  其實隔離性的實現原理就是,於是隔離性也能夠稱爲併發控制、鎖等。事務的隔離性要求每一個讀寫事務的對象對其餘事務的操做對象能互相分離。再者,好比操做緩衝池中的LRU列表,刪除,添加、移動LRU列表中的元素,爲了保證一致性那麼就要鎖的介入。InnoDB使用鎖爲了支持對共享資源進行併發訪問,提供數據的完整性和一致性。那麼到底InnoDB支持什麼樣的鎖呢?咱們先來看下InnoDB的鎖的介紹:mysql

InnoDB中的鎖

  你可能聽過各類各樣的InnoDB的數據庫鎖,gap鎖啊,共享鎖,排它鎖,讀鎖,寫鎖等等。可是InnoDB的標準實現的鎖只有2類,一種是行級鎖,一種是意向鎖。算法

  InnoDB實現了以下兩種標準的行級鎖sql

  • 共享鎖(讀鎖 S Lock),容許事務讀一行數據
  • 排它鎖(寫鎖 X Lock),容許事務刪除一行數據或者更新一行數據

  行級鎖中,除了S和S兼容,其餘都不兼容。數據庫

  InnoDB支持兩種意向鎖(即爲表級別的鎖):session

  • 意向共享鎖(讀鎖 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走的非惟一索引、主鍵索引。前置條件爲事務隔離級別爲RR且sql走的非惟一索引、主鍵索引。前置條件爲事務隔離級別爲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操做以前,還會加一種鎖,官方文檔稱它爲insertion 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表來分析一下數據庫死鎖,比較好理解:

四種隔離級別

  那麼按照最嚴格到最鬆的順序來說一下四種隔離級別

1.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

2.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)

3.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)

 

4.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://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html

http://hedengcheng.com/?p=771#_%E7%BB%84%E5%90%88%E4%B8%89%EF%BC%9Aid%E9%9D%9E%E5%94%AF%E4%B8%80%E7%B4%A2%E5%BC%95+RC

http://www.javashuo.com/article/p-optsgnif-k.html

https://www.zhihu.com/question/51513268/answer/127777478

相關文章
相關標籤/搜索