前言
呵呵 最近突然想起了 數據庫相關的問題似乎是在一些場景下面問的比較多, 特別是 事務的隔離級別, sql 的優化呀, 這些東西
於是 最近就需要補一下 這方面的知識了, 本文便是其中之一
不同的事務可重複讀的處理, mysql 裏面的是很經典的解決方案, 那麼 我們便來看一下吧
閱讀本文之前, 最好可以先看一下 參考的相關文章, 這樣會更加輕鬆一些
看下文章 MySQL多版本併發控制機制(MVCC)-源碼淺析 是收貨頗多的
環境說明
gif來自於(mac上面的軟件錄製gif太大了)
root@server:~# cat /proc/version
Linux version 4.4.0-138-generic (buildd@lcy01-amd64-006) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.10) ) #164-Ubuntu SMP Tue Oct 2 17:16:02 UTC 2018
mysql> select version();
+-----------------------------+
| version() |
+-----------------------------+
| 5.7.24-0ubuntu0.16.04.1-log |
+-----------------------------+
1 row in set (0.00 sec)
相關代碼, 調試信息來自
master:~ jerry$ cat /System/Library/CoreServices/SystemVersion.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ProductBuildVersion</key>
<string>18E2035</string>
<key>ProductCopyright</key>
<string>1983-2019 Apple Inc.</string>
<key>ProductName</key>
<string>Mac OS X</string>
<key>ProductUserVisibleVersion</key>
<string>10.14.4</string>
<key>ProductVersion</key>
<string>10.14.4</string>
<key>iOSSupportVersion</key>
<string>12.2</string>
</dict>
</plist>
mysql> select version();
+--------------+
| version() |
+--------------+
| 5.6.33-debug |
+--------------+
1 row in set (0.00 sec)
測試的表結構
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`age` int(11) DEFAULT 0,
`name` varchar(64),
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8 ;
表結構如上
-- 重新構造測試數據
delete from user;
insert into user values ('1', '27', 'jerry');
重置測試數據的 sql 如上
下文需要用到的相關代碼如下
readOread.read_view_open_now_low : 創建 consistent view
readOread.read_view_sees_tx_id : 判斷當前 view 是否可讀給定的事務 對應的記錄
case01 tx1, 新增, tx2 查詢, tx1 commit, tx2 查詢
# step 0 : 查詢 select * from user where id = '1';
事務版本號爲 : 3849
rec(id = 1) 的 trx_id 爲 2823
默認情況下(沒有手動開啓事務), 每一次查詢都會創建 consistent read view
# step 1 : 開啓事務 1, 事務 2, 並且事務 1, 事務 2, 執行查詢 select * from user where id = '1';
事務 1 執行查詢 : 創建 consistent read view, trx_id 爲 3850
view = {read_view_t * | 0x7f94b5f8b998} 0x00007f94b5f8b998
type = {ulint} 1
undo_no = {undo_no_t} 0
low_limit_no = {trx_id_t} 3851
low_limit_id = {trx_id_t} 3851
up_limit_id = {trx_id_t} 3851
n_trx_ids = {ulint} 0
trx_ids = {trx_id_t * | 0x7f94b5f8b9e8} 0x00007f94b5f8b9e8
creator_trx_id = {trx_id_t} 3850
view_list = {ut_list_node<read_view_t>}
prev = {read_view_t * | 0x0} NULL
next = {read_view_t * | 0x0} NULL
rec(id = 1) 的 trx_id 爲 2823, 對當前事務可見, 返回結果
# 事務 2 執行查詢 : 創建 consistent read view, trx_id 爲 3851
view = {read_view_t * | 0x7f94b8c58818} 0x00007f94b8c58818
type = {ulint} 1
undo_no = {undo_no_t} 0
low_limit_no = {trx_id_t} 3852
low_limit_id = {trx_id_t} 3852
up_limit_id = {trx_id_t} 3850
n_trx_ids = {ulint} 1
trx_ids = {trx_id_t * | 0x7f94b8c58868} 0x00007f94b8c58868
*trx_ids = {trx_id_t} 3850
creator_trx_id = {trx_id_t} 3851
view_list = {ut_list_node<read_view_t>}
prev = {read_view_t * | 0x0} NULL
next = {read_view_t * | 0x00007f94b5f8b998} 0x00007f94b5f8b998
rec(id = 1) 的 trx_id 爲 2823, 對當前事務可見, 返回結果
# step 2, 事務 1 再一次執行 select * from user where id = '1';
view = {read_view_t * | 0x7f94b5f8b998} 0x00007f94b5f8b998
type = {ulint} 1
undo_no = {undo_no_t} 0
low_limit_no = {trx_id_t} 3851
low_limit_id = {trx_id_t} 3851
up_limit_id = {trx_id_t} 3851
n_trx_ids = {ulint} 0
trx_ids = {trx_id_t * | 0x7f94b5f8b9e8} 0x00007f94b5f8b9e8
creator_trx_id = {trx_id_t} 3850
view_list = {ut_list_node<read_view_t>}
prev = {read_view_t * | 0x0} NULL
next = {read_view_t * | 0x0} NULL
可以看到這個視圖 和 第一次查詢的時候 一模一樣, 因爲 REPEATABLE-READ 的一次事務只會創建一個 consistent read view
# step 3, 事務 1 執行 insert into user values ('2', '28', 'lucy');
# step 4, 事務 1 執行 select * from user where id = '2';
rec(id = 1) 的 trx_id 爲 3850, 對當前事務可見, 返回結果
# step 5, 事務 2 執行 select * from user where id = '2';
rec(id = 1) 的 trx_id 爲 3850, 對當前事務不可見, 不返回結果
# step 6, 事務1 執行 commit;
# step 7, 事務2 執行 select * from user where id = '2';
因爲 view.trx_ids 中依然記錄了 3850, 經過 read_view_sees_trx_id 的判斷, 還是看不見
case02 tx1, 更新, tx2 查詢
# step 0 : 查詢 select * from user where id = '1';
事務版本號爲 : 3861
rec(id = 1) 的 trx_id 爲 3860
# step 1, 開啓事務 1, 事務 2, 並且事務 1, 事務 2, 執行查詢 select * from user where id = '1';
事務 1 執行查詢 : 創建 consistent read view, trx_id 爲 3862
view = {read_view_t * | 0x7f94b5f8b998} 0x00007f94b5f8b998
type = {ulint} 1
undo_no = {undo_no_t} 0
low_limit_no = {trx_id_t} 3863
low_limit_id = {trx_id_t} 3863
up_limit_id = {trx_id_t} 3863
n_trx_ids = {ulint} 0
trx_ids = {trx_id_t * | 0x7f94b5f8b9e8} 0x00007f94b5f8b9e8
*trx_ids = {trx_id_t} 0
creator_trx_id = {trx_id_t} 3862
view_list = {ut_list_node<read_view_t>}
prev = {read_view_t * | 0x0} NULL
next = {read_view_t * | 0x0} NULL
事務 2 執行查詢 : 創建 consistent read view, trx_id 爲 3863
view = {read_view_t * | 0x7f94b8c58818} 0x00007f94b8c58818
type = {ulint} 1
undo_no = {undo_no_t} 0
low_limit_no = {trx_id_t} 3864
low_limit_id = {trx_id_t} 3864
up_limit_id = {trx_id_t} 3862
n_trx_ids = {ulint} 1
trx_ids = {trx_id_t * | 0x7f94b8c58868} 0x00007f94b8c58868
*trx_ids = {trx_id_t} 3862
creator_trx_id = {trx_id_t} 3863
view_list = {ut_list_node<read_view_t>}
prev = {read_view_t * | 0x0} NULL
next = {read_view_t * | 0x7f94b5f8b998} 0x7f94b5f8b998
# step 2, tx 1 執行 update user set name = 'lucy' where id = '1';
# step 3, tx 1 執行 select * from user where id = '1';
rec(id = 1) 的 trx_id 爲 3862, 對當前事務可見, 返回結果, 結果爲 step 2 更新之後的結果
# step 4, tx 2 執行 select * from user where id = '1';
rec(id = 1) 的 trx_id 爲 3862, 經過 read_view_sees_trx_id 判斷, 對當前事務不可見
將 rec(id = 1) 回退到上一個版本, 3860, 經過 read_view_sees_trx_id 判斷, 對當前事務可見, 返回這個版本的結果
# step 5, 事務1 執行 commit;
# step 6, tx 2 執行 select * from user where id = '1';
rec(id = 1) 的 trx_id 爲 3862, 經過 read_view_sees_trx_id 判斷, 對當前事務不可見
將 rec(id = 1) 回退到上一個版本, 3860, 經過 read_view_sees_trx_id 判斷, 對當前事務可見, 返回這個版本的結果
case03 tx1, 刪除, tx2 查詢
# step 1, 開啓事務 1, 事務 2, 並且事務 1, 事務 2, 執行查詢 select * from user where id = '1';
事務 1 執行查詢 : 創建 consistent read view, trx_id 爲 3876
view = {read_view_t * | 0x7f94b5f8b998} 0x00007f94b5f8b998
type = {ulint} 1
undo_no = {undo_no_t} 0
low_limit_no = {trx_id_t} 3877
low_limit_id = {trx_id_t} 3877
up_limit_id = {trx_id_t} 3877
n_trx_ids = {ulint} 0
trx_ids = {trx_id_t * | 0x7f94b5f8b9e8} 0x00007f94b5f8b9e8
*trx_ids = {trx_id_t} 3863
creator_trx_id = {trx_id_t} 3876
view_list = {ut_list_node<read_view_t>}
prev = {read_view_t * | 0x0} NULL
next = {read_view_t * | 0x0} NULL
rec(id = 1) 的 trx_id 爲 3873
事務 2 執行查詢 : 創建 consistent read view, trx_id 爲 3877
view = {read_view_t * | 0x7f94b8c58818} 0x00007f94b8c58818
type = {ulint} 1
undo_no = {undo_no_t} 0
low_limit_no = {trx_id_t} 3878
low_limit_id = {trx_id_t} 3878
up_limit_id = {trx_id_t} 3876
n_trx_ids = {ulint} 1
trx_ids = {trx_id_t * | 0x7f94b8c58868} 0x00007f94b8c58868
*trx_ids = {trx_id_t} 3876
creator_trx_id = {trx_id_t} 3877
view_list = {ut_list_node<read_view_t>}
prev = {read_view_t * | 0x0} NULL
next = {read_view_t * | 0x00007f94b5f8b998} 0x00007f94b5f8b998
# step 2, 事務 1 執行, delete from user where id = '1';
# step 3, 事務 1 執行, select * from user where id = '1';
rec(id = 1) 的 trx_id 爲 3876, 對於當前事務 可見, 返回的結果爲 Empty set (2 min 5.65 sec)(標記刪除)
# step 4, 事務 2 執行, select * from user where id = '1';
rec(id = 1) 的 trx_id 爲 3876, 對於當前事務 不可見
將 rec(id = 1) 回退到上一個版本, 3873, 經過 read_view_sees_trx_id 判斷, 對當前事務可見, 返回這個版本的結果
# step 5, 事務1 執行 commit;
# step 6, tx 2 執行 select * from user where id = '1';
rec(id = 1) 的 trx_id 爲 3876, 對於當前事務 不可見
將 rec(id = 1) 回退到上一個版本, 3873, 經過 read_view_sees_trx_id 判斷, 對當前事務可見, 返回這個版本的結果
case04 tx1, 新增, tx2 更新(經典的幻讀場景)
# step 1, 開啓事務 1, 事務 2, 並且事務 1, 事務 2, 執行查詢 select * from user where id = '1';
事務 1 執行查詢 : 創建 consistent read view, trx_id 爲 7522
view = {read_view_t * | 0x7fcbef7b5ca8} 0x00007fcbef7b5ca8
type = {ulint} 1
undo_no = {undo_no_t} 0
low_limit_no = {trx_id_t} 7523
low_limit_id = {trx_id_t} 7523
up_limit_id = {trx_id_t} 7523
n_trx_ids = {ulint} 0
trx_ids = {trx_id_t * | 0x7fcbef7b5cf8} 0x00007fcbef7b5cf8
*trx_ids = {trx_id_t} 7497
creator_trx_id = {trx_id_t} 7522
view_list = {ut_list_node<read_view_t>}
prev = {read_view_t * | 0x0} NULL
next = {read_view_t * | 0x0} NULL
rec(id = 1) 的 trx_id 爲 7515
事務 2 執行查詢 : 創建 consistent read view, trx_id 爲 7523
view = {read_view_t * | 0x7fcbef04ce48} 0x00007fcbef04ce48
type = {ulint} 1
undo_no = {undo_no_t} 0
low_limit_no = {trx_id_t} 7524
low_limit_id = {trx_id_t} 7524
up_limit_id = {trx_id_t} 7522
n_trx_ids = {ulint} 1
trx_ids = {trx_id_t * | 0x7fcbef04ce98} 0x00007fcbef04ce98
*trx_ids = {trx_id_t} 7522
creator_trx_id = {trx_id_t} 7523
view_list = {ut_list_node<read_view_t>}
prev = {read_view_t * | 0x0} NULL
next = {read_view_t * | 0x7fcbef7b5ca8} 0x00007fcbef7b5ca8
# step 2, tx 1 執行 insert into user values ('2', '28', 'lucy');
# step 3, tx 1 執行 select * from user;
rec(id = 1) 的 trx_id 爲 7515, 對 tx1 可見
rec(id = 2) 的 trx_id 爲 7522, 對 tx1 可見
# step 4, tx 2 執行 select * from user;
rec(id = 1) 的 trx_id 爲 7515, 對 tx2 可見
rec(id = 2) 的 trx_id 爲 7522, 對 tx2 不可見
# step 5, tx 1 執行 commit ;(先執行 commit, 再執行 update, 不然會造成 Lock wait timeout exceeded;)
# step 6, tx 2 執行 update user set name = 'memcpy';
從日誌中可以看到 更新了兩條記錄
Query OK, 2 rows affected (3.87 sec)
Rows matched: 2 Changed: 2 Warnings: 0
# step7, tx2 執行 select * from user;
rec(id = 1) 的 trx_id 爲 7523, 對 tx2 可見
rec(id = 2) 的 trx_id 爲 7523, 對 tx2 可見
結果爲 更新之後的數據
一下內容引用自 MySQL多版本併發控制機制(MVCC)-源碼淺析
MySQL是通過MVCC和二階段鎖(2PL)來兼顧性能和一致性的,但是由於MySQL僅僅在select時候才創建一致性視圖,而在update等加鎖操作的時候並不做如此操作,所以就會產生一些詭異的現象。如下圖所示:
如果理解了update不走一致性視圖(read_view),而select走一致性視圖(read_view),就可以很好解釋這個現象。 如下圖所示:
case05 tx1, 更新, tx2 新增(經典的幻讀場景)
# step 1, 開啓事務 1, 事務 2, 並且事務 1, 事務 2, 執行查詢 select * from user where id = '1';
事務 1 執行查詢 : 創建 consistent read view, trx_id 爲 7549
view = {read_view_t * | 0x7fcbef7b5ca8} 0x00007fcbef7b5ca8
type = {ulint} 1
undo_no = {undo_no_t} 0
low_limit_no = {trx_id_t} 7550
low_limit_id = {trx_id_t} 7550
up_limit_id = {trx_id_t} 7550
n_trx_ids = {ulint} 0
trx_ids = {trx_id_t * | 0x7fcbef7b5cf8} 0x00007fcbef7b5cf8
*trx_ids = {trx_id_t} 7497
creator_trx_id = {trx_id_t} 7549
view_list = {ut_list_node<read_view_t>}
prev = {read_view_t * | 0x0} NULL
next = {read_view_t * | 0x0} NULL
rec(id = 1) 的 trx_id 爲 7542
事務 2 執行查詢 : 創建 consistent read view, trx_id 爲 7550
view = {read_view_t * | 0x7fcbef04ce48} 0x00007fcbef04ce48
type = {ulint} 1
undo_no = {undo_no_t} 0
low_limit_no = {trx_id_t} 7551
low_limit_id = {trx_id_t} 7551
up_limit_id = {trx_id_t} 7549
n_trx_ids = {ulint} 1
trx_ids = {trx_id_t * | 0x7fcbef04ce98} 0x00007fcbef04ce98
*trx_ids = {trx_id_t} 7549
creator_trx_id = {trx_id_t} 7550
view_list = {ut_list_node<read_view_t>}
prev = {read_view_t * | 0x0} NULL
next = {read_view_t * | 0x7fcbef7b5ca8} 0x00007fcbef7b5ca8
# step 2, 事務1 執行 update user set name = 'memcpy';
Query OK, 1 row affected (8.84 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# step 3, 事務1 執行 select * from user;
rec(id = 1) 的 trx_id 爲 7549, 對 tx1 可見
# step 4, 事務2 執行 select * from user;
rec(id = 1) 的 trx_id 爲 7549, 對 tx2 不可見
將 rec(id = 1) 回退到上一個版本, 7542, 經過 read_view_sees_trx_id 判斷, 對當前事務可見, 返回這個版本的結果
# step 5, 事務1 執行 commit (先執行 commit, 再執行 insert, 不然會造成 Lock wait timeout exceeded;)
# step 6, 事務2 執行 insert into user values ('2', '28', 'lucy');
rec(id = 1) 的 trx_id 爲 7549, 對 tx2 不可見
將 rec(id = 1) 回退到上一個版本, 7542, 經過 read_view_sees_trx_id 判斷, 對當前事務可見, 返回這個版本的結果
rec(id = 2) 的 trx_id 爲 7550, 對 tx2 可見
# step 7, 窗口1 執行 select * from user;
新創建了一個 consistent read view, trx_id 爲 7558
rec(id = 1) 的 trx_id 爲 7549, 對 當前事務 可見
rec(id = 2) 的 trx_id 爲 7550, 對 當前事務 可見
mysql> select * from user;
+----+------+--------+
| id | age | name |
+----+------+--------+
| 1 | 27 | memcpy |
| 2 | 28 | lucy |
+----+------+--------+
以上, 調試截取了一部分運行時數據來分析可重複讀的幾種經典的場景
完
參考
innodb事務開啓後,修改一行記錄,是update執行後行版本號增加,還是在事物提交後行版本號增加?
14.7.2.3 Consistent Nonlocking Reads