一文快速搞懂MySQLInnoDB事務(wù)ACID實現(xiàn)原理

【51CTO.com原創(chuàng)稿件】說到數(shù)據(jù)庫事務(wù),想到的就是要么都做修改,要么都不做,或者是 ACID 的概念。其實事務(wù)的本質(zhì)就是鎖、并發(fā)和重做日志的結(jié)合體。

成都創(chuàng)新互聯(lián)專注為客戶提供全方位的互聯(lián)網(wǎng)綜合服務(wù),包含不限于成都做網(wǎng)站、網(wǎng)站建設(shè)、安寧網(wǎng)絡(luò)推廣、小程序制作、安寧網(wǎng)絡(luò)營銷、安寧企業(yè)策劃、安寧品牌公關(guān)、搜索引擎seo、人物專訪、企業(yè)宣傳片、企業(yè)代運營等,從售前售中售后,我們都將竭誠為您服務(wù),您的肯定,是我們最大的嘉獎;成都創(chuàng)新互聯(lián)為所有大學生創(chuàng)業(yè)者提供安寧建站搭建服務(wù),24小時服務(wù)熱線:18980820575,官方網(wǎng)址:muchs.cn

這一篇主要講一下 InnoDB 中的事務(wù)到底是如何實現(xiàn) ACID 的:

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

    
    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 插入數(shù)據(jù):

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


明明插入的數(shù)據(jù)和鎖住的數(shù)據(jù)沒有毛線關(guān)系,為什么還會阻塞等鎖最后超時呢?這就是 Next-Key Lock 實現(xiàn)的。
畫張圖你就明白了:![](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,其實就是防止幻讀(同一事務(wù)下,連續(xù)執(zhí)行兩句同樣的 SQL 得到不同的結(jié)果)。
為了保證圖上 3 個小箭頭中間不會插入滿足條件的新記錄,所以用到了 Gap 鎖防止幻讀。
簡單的 Insert 會在 Insert 的行對應(yīng)的索引記錄上加一個 Record Lock 鎖,并沒有 Gap 鎖,所以并不會阻塞其他 Session 在 Gap 間隙里插入記錄。
不過在 Insert 操作之前,還會加一種鎖,官方文檔稱它為 Intention Gap Lock,也就是意向的 Gap 鎖。
這個意向 Gap 鎖的作用就是預示著當多事務(wù)并發(fā)插入相同的 Gap 空隙時,只要插入的記錄不是 Gap 間隙中的相同位置,則無需等待其他 Session 就可完成,這樣就使得 Insert 操作無須加真正的 Gap Lock。
Session A 插入數(shù)據(jù):
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 插入數(shù)據(jù),完全沒有問題,沒有阻塞:
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 是如何加鎖的,現(xiàn)在可以去嘗試分析死鎖。死鎖的本質(zhì)就是兩個事務(wù)相互等待對方釋放持有的鎖導致的,關(guān)鍵在于不同 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 表來分析一下數(shù)據(jù)庫死鎖,比較好理解:![](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(可序列化)**
最高事務(wù)隔離級別。主要用在 InnoDB 存儲引擎的分布式事務(wù)。強制事務(wù)排序,串行化執(zhí)行事務(wù)。
不需要沖突控制,但是慢速設(shè)備。根據(jù) Jim Gray 在《Transaction Processing》一書中指出,Read Committed 和 Serializable 的開銷幾乎是一樣的,甚至 Serializable 更優(yōu)。
Session A 設(shè)置隔離級別為 Serializable,并開始事務(wù)執(zhí)行一句 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 一條數(shù)據(jù),超時:
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(可重復讀)**
一個事務(wù)按相同的查詢條件讀取以前檢索過的數(shù)據(jù),其他事務(wù)插入了滿足其查詢條件的新數(shù)據(jù),產(chǎn)生幻讀。
InnoDB 存儲引擎在 RR 隔離級別下,已經(jīng)使用 Next-Key Lock 算法避免了幻讀,了解概念即可。
InnoDB 使用 MVCC 來讀取數(shù)據(jù),RR 隔離級別下,總是讀取事務(wù)開始時的行數(shù)據(jù)版本。
Session A 查看 id=1 的數(shù)據(jù):
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 的數(shù)據(jù):
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 
然后現(xiàn)在 Session A 再查看一下 id=1 的數(shù)據(jù),數(shù)據(jù)還是事務(wù)開始時候的數(shù)據(jù)。
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(讀已提交)**
事務(wù)從開始直到提交之前,所做的任何修改對其他事務(wù)都是不可見的。
InnoDB 使用 MVCC 來讀取數(shù)據(jù),RC 隔離級別下,總是讀取被鎖定行最新的快照數(shù)據(jù)。
Session A 查看 id=1 的數(shù)據(jù):
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 的記錄,發(fā)現(xiàn)數(shù)據(jù)已經(jīng)是最新的數(shù)據(jù):
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(讀未提交)**
事務(wù)中的修改,即使沒有提交,對其他事務(wù)也都是可見的。
Session A 查看一下 id=3 的數(shù)據(jù),沒有 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 的數(shù)據(jù),但是沒有 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 再次查看則看到了新的結(jié)果:
mysql> select * from m_test_db.M where id =3; 
+----+---------+---------+ 
| id | user_id | name    | 
+----+---------+---------+ 
|  3 | 23      | GRIMMJX | 
+----+---------+---------+ 
1 row in set (0.00 sec) 
這里花了很多筆墨來介紹隔離性,這是比較重要,需要靜下心來學習的特性。所以也是放在第一個的原因。
**原子性、一致性、持久性**
事務(wù)隔離性由鎖實現(xiàn),原子性、一致性和持久性由數(shù)據(jù)庫的 redo log 和 undo log 實現(xiàn)。
redo log 稱為重做日志,用來保證事務(wù)的原子性和持久性,恢復提交事務(wù)修改的頁操作。
undo log 來保證事務(wù)的一致性,undo 回滾行記錄到某個特性版本及 MVCC 功能。兩者內(nèi)容不同。redo 記錄物理日志,undo 是邏輯日志。
**redo**
重做日志由重做日志緩沖(redo log buffer)和重做日志文件(redo log file)組成,前者是易失的,后者是持久的。
InnoDB 通過 Force Log at Commit 機制來實現(xiàn)持久性,當 Commit 時,必須先將事務(wù)的所有日志寫到重做日志文件進行持久化,待 Commit 操作完成才算完成。
當事務(wù)提交時,日志不寫入重做日志文件,而是等待一個事件周期后再執(zhí)行 Fsync 操作,由于并非強制在事務(wù)提交時進行一次 Fsync 操作,顯然這可以提高數(shù)據(jù)庫性能。
請記住 3 點:
重做日志是在 InnoDB 層產(chǎn)生的。
重做日志是物理格式日志,記錄的是對每個頁的修改。
重做日志在事務(wù)進行中不斷被寫入。
**undo**
事務(wù)回滾和 MVCC,這就需要 undo。undo 是邏輯日志,只是將數(shù)據(jù)庫邏輯恢復到原來的樣子,但是數(shù)據(jù)結(jié)構(gòu)和頁本身在回滾之后可能不同。
例如:用戶執(zhí)行 insert 10w 條數(shù)據(jù)的事務(wù),表空間因而增大。用戶執(zhí)行 ROLLBACK 之后,會對插入的數(shù)據(jù)回滾,但是表空間大小不會因此收縮。
實際的做法就是做與之前想法的操作,Insert 對應(yīng) Delete,Update 對應(yīng)反向 Update 來實現(xiàn)原子性。
InnoDB 中 MVCC 的實現(xiàn)就是靠 undo,舉個經(jīng)典的例子:Bob 給 Smith 轉(zhuǎn) 100 元,那么就存在以下 3 個版本,RR 隔離級別下,對于快照數(shù)據(jù),總是讀事務(wù)開始的行數(shù)據(jù)版本見黃標。
RC 隔離級別下,對于快照數(shù)據(jù),總是讀最新的一份快照數(shù)據(jù)見紅標:
![](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 會產(chǎn)生 redo log,因為 undo log 需要持久性保護 。
最后,你會發(fā)現(xiàn)姜承堯的 MySQL InnoDB 書上的很多內(nèi)容都是官方手冊的翻譯,無論是看源碼還是學習新框架,最好看原汁原味的。
只要你堅持,一步一步來,總歸會成功的。切忌,學技術(shù)急不來,快就是穩(wěn),穩(wěn)就是快。

新聞名稱:一文快速搞懂MySQLInnoDB事務(wù)ACID實現(xiàn)原理
瀏覽地址:http://muchs.cn/article6/isgiig.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供面包屑導航定制網(wǎng)站、定制開發(fā)、網(wǎng)站設(shè)計公司、建站公司、軟件開發(fā)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

網(wǎng)站優(yōu)化排名