01 mysql可重複讀的幾種情況的調試

前言

呵呵 最近突然想起了 數據庫相關的問題似乎是在一些場景下面問的比較多, 特別是 事務的隔離級別, 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   |
    +----+------+--------+

 

以上, 調試截取了一部分運行時數據來分析可重複讀的幾種經典的場景 

 

 

完 

 

 

參考

MySQL多版本併發控制機制(MVCC)-源碼淺析

初探InnoDB MVCC源碼實現

MVCC原理探究及MySQL源碼實現分析

mvcc 【下】源碼分析原理

innodb事務開啓後,修改一行記錄,是update執行後行版本號增加,還是在事物提交後行版本號增加?

14.7.2.3 Consistent Nonlocking Reads

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章