【51CTO.com原創(chuàng)稿件】說到數(shù)據(jù)庫(kù)事務(wù),想到的就是要么都做修改,要么都不做,或者是 ACID 的概念。其實(shí)事務(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íng)銷、安寧企業(yè)策劃、安寧品牌公關(guān)、搜索引擎seo、人物專訪、企業(yè)宣傳片、企業(yè)代運(yùn)營(yíng)等,從售前售中售后,我們都將竭誠(chéng)為您服務(wù),您的肯定,是我們最大的嘉獎(jiǎng);成都創(chuàng)新互聯(lián)為所有大學(xué)生創(chuàng)業(yè)者提供安寧建站搭建服務(wù),24小時(shí)服務(wù)熱線:18980820575,官方網(wǎng)址:www.cdcxhl.com
這一篇主要講一下 InnoDB 中的事務(wù)到底是如何實(shí)現(xiàn) ACID 的:
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,強(qiáng)制走這個(gè)非唯一輔助索引,因?yàn)檫@張表里的數(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)系,為什么還會(huì)阻塞等鎖最后超時(shí)呢?這就是 Next-Key Lock 實(shí)現(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,其實(shí)就是防止幻讀(同一事務(wù)下,連續(xù)執(zhí)行兩句同樣的 SQL 得到不同的結(jié)果)。
為了保證圖上 3 個(gè)小箭頭中間不會(huì)插入滿足條件的新記錄,所以用到了 Gap 鎖防止幻讀。
簡(jiǎn)單的 Insert 會(huì)在 Insert 的行對(duì)應(yīng)的索引記錄上加一個(gè) Record Lock 鎖,并沒有 Gap 鎖,所以并不會(huì)阻塞其他 Session 在 Gap 間隙里插入記錄。
不過在 Insert 操作之前,還會(huì)加一種鎖,官方文檔稱它為 Intention Gap Lock,也就是意向的 Gap 鎖。
這個(gè)意向 Gap 鎖的作用就是預(yù)示著當(dāng)多事務(wù)并發(fā)插入相同的 Gap 空隙時(shí),只要插入的記錄不是 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ì)就是兩個(gè)事務(wù)相互等待對(duì)方釋放持有的鎖導(dǎo)致的,關(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ù)庫(kù)死鎖,比較好理解:![](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=)
**四種隔離級(jí)別**
那么按照最嚴(yán)格到最松的順序來講一下四種隔離級(jí)別:
**①Serializable(可序列化)**
最高事務(wù)隔離級(jí)別。主要用在 InnoDB 存儲(chǔ)引擎的分布式事務(wù)。強(qiáng)制事務(wù)排序,串行化執(zhí)行事務(wù)。
不需要沖突控制,但是慢速設(shè)備。根據(jù) Jim Gray 在《Transaction Processing》一書中指出,Read Committed 和 Serializable 的開銷幾乎是一樣的,甚至 Serializable 更優(yōu)。
Session A 設(shè)置隔離級(jí)別為 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ù),超時(shí):
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(可重復(fù)讀)**
一個(gè)事務(wù)按相同的查詢條件讀取以前檢索過的數(shù)據(jù),其他事務(wù)插入了滿足其查詢條件的新數(shù)據(jù),產(chǎn)生幻讀。
InnoDB 存儲(chǔ)引擎在 RR 隔離級(jí)別下,已經(jīng)使用 Next-Key Lock 算法避免了幻讀,了解概念即可。
InnoDB 使用 MVCC 來讀取數(shù)據(jù),RR 隔離級(jí)別下,總是讀取事務(wù)開始時(shí)的行數(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í)候的數(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ù)從開始直到提交之前,所做的任何修改對(duì)其他事務(wù)都是不可見的。
InnoDB 使用 MVCC 來讀取數(shù)據(jù),RC 隔離級(jí)別下,總是讀取被鎖定行最新的快照數(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ù)中的修改,即使沒有提交,對(duì)其他事務(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)
這里花了很多筆墨來介紹隔離性,這是比較重要,需要靜下心來學(xué)習(xí)的特性。所以也是放在第一個(gè)的原因。
**原子性、一致性、持久性**
事務(wù)隔離性由鎖實(shí)現(xiàn),原子性、一致性和持久性由數(shù)據(jù)庫(kù)的 redo log 和 undo log 實(shí)現(xiàn)。
redo log 稱為重做日志,用來保證事務(wù)的原子性和持久性,恢復(fù)提交事務(wù)修改的頁(yè)操作。
undo log 來保證事務(wù)的一致性,undo 回滾行記錄到某個(gè)特性版本及 MVCC 功能。兩者內(nèi)容不同。redo 記錄物理日志,undo 是邏輯日志。
**redo**
重做日志由重做日志緩沖(redo log buffer)和重做日志文件(redo log file)組成,前者是易失的,后者是持久的。
InnoDB 通過 Force Log at Commit 機(jī)制來實(shí)現(xiàn)持久性,當(dāng) Commit 時(shí),必須先將事務(wù)的所有日志寫到重做日志文件進(jìn)行持久化,待 Commit 操作完成才算完成。
當(dāng)事務(wù)提交時(shí),日志不寫入重做日志文件,而是等待一個(gè)事件周期后再執(zhí)行 Fsync 操作,由于并非強(qiáng)制在事務(wù)提交時(shí)進(jìn)行一次 Fsync 操作,顯然這可以提高數(shù)據(jù)庫(kù)性能。
請(qǐng)記住 3 點(diǎn):
重做日志是在 InnoDB 層產(chǎn)生的。
重做日志是物理格式日志,記錄的是對(duì)每個(gè)頁(yè)的修改。
重做日志在事務(wù)進(jìn)行中不斷被寫入。
**undo**
事務(wù)回滾和 MVCC,這就需要 undo。undo 是邏輯日志,只是將數(shù)據(jù)庫(kù)邏輯恢復(fù)到原來的樣子,但是數(shù)據(jù)結(jié)構(gòu)和頁(yè)本身在回滾之后可能不同。
例如:用戶執(zhí)行 insert 10w 條數(shù)據(jù)的事務(wù),表空間因而增大。用戶執(zhí)行 ROLLBACK 之后,會(huì)對(duì)插入的數(shù)據(jù)回滾,但是表空間大小不會(huì)因此收縮。
實(shí)際的做法就是做與之前想法的操作,Insert 對(duì)應(yīng) Delete,Update 對(duì)應(yīng)反向 Update 來實(shí)現(xiàn)原子性。
InnoDB 中 MVCC 的實(shí)現(xiàn)就是靠 undo,舉個(gè)經(jīng)典的例子:Bob 給 Smith 轉(zhuǎn) 100 元,那么就存在以下 3 個(gè)版本,RR 隔離級(jí)別下,對(duì)于快照數(shù)據(jù),總是讀事務(wù)開始的行數(shù)據(jù)版本見黃標(biāo)。
RC 隔離級(jí)別下,對(duì)于快照數(shù)據(jù),總是讀最新的一份快照數(shù)據(jù)見紅標(biāo):
![](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 會(huì)產(chǎn)生 redo log,因?yàn)?undo log 需要持久性保護(hù) 。
最后,你會(huì)發(fā)現(xiàn)姜承堯的 MySQL InnoDB 書上的很多內(nèi)容都是官方手冊(cè)的翻譯,無論是看源碼還是學(xué)習(xí)新框架,最好看原汁原味的。
只要你堅(jiān)持,一步一步來,總歸會(huì)成功的。切忌,學(xué)技術(shù)急不來,快就是穩(wěn),穩(wěn)就是快。