您好,登錄后才能下訂單哦!
【51CTO.com原創稿件】說到數據庫事務,想到的就是要么都做修改,要么都不做,或者是 ACID 的概念。其實事務的本質就是鎖、并發和重做日志的結合體。
這一篇主要講一下 InnoDB 中的事務到底是如何實現 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,強制走這個非唯一輔助索引,因為這張表里的數據很少。
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】
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。