天天看點

從一個案例深入剖析InnoDB隐式鎖和可見性判斷(1)

一、問題抛出

最近遇到一個問題,得到棧如下(5.6.25):

從一個案例深入剖析InnoDB隐式鎖和可見性判斷(1)
出現這個問題的時候隻存在一個讀寫事務,那就是本事務。對這裡的紅色部分比較感興趣,但是這裡不是所有的内容都和這個問題相關,主要還是圍繞可見性判斷和隐式鎖判定進行,算是我的思考過程。但是對Innodb認知水準有限,如有誤導請諒解。使用的源碼版本5.7.29。

二、read view 簡述

關于read view說明的文章已經很多了,我這裡簡單記錄一下我學習的地方。一緻性讀取(consistent read),根據隔離級别的不同,會在不同的時機建立read view,如下:

  • RR 事務的第一個select指令發起的時候建立read view,直到事務送出釋放
  • RC 事務的每一個select都會單獨建立read view

有了read view 就能夠對每行資料的可見性進行判斷了,下面是read view中的關鍵屬性

  • m_up_limit_id:如果行的trx id 小于了m_up_limit_id則不可見。
  • m_low_limit_id:如果行的trx id 大于了m_low_limit_id則可見。
  • m_ids:是用于記錄建立read view時刻的讀寫事務的vector數組,用于對于m_up_limit_id和m_low_limit_id之間的trx需要根據它來進行判定,是否處于活躍狀态。
  • m_low_limit_no則用于記錄建立read view時刻的最小trx no,主要用于purge線程判斷清理undo使用。

如何拿到值得具體可以參見附錄,而對于可見性的判斷我們可以參考如下函數:

/** Check whether the changes by id are visible.
 @param[in] id transaction id to check against the view
 @param[in] name table name
 @return whether the view sees the modifications of id. */
 bool changes_visible(
  trx_id_t  id,
  const table_name_t& name) const
  MY_ATTRIBUTE((warn_unused_result))
 {
  ut_ad(id > 0);
  if (id < m_up_limit_id || id == m_creator_trx_id) { //小于 可見
   return(true);
  }
  check_trx_id_sanity(id, name);
  if (id >= m_low_limit_id) { //大于不可見
   return(false);
  } else if (m_ids.empty()) { //如果之間的 active 為空 則可見 
   return(true);
  }
  const ids_t::value_type* p = m_ids.data();
  return(!std::binary_search(p, p + m_ids.size(), id)); //否則比較本trx id 是否在這之中,如果在不可以見,反之可見
 }      

三、關于可見性判斷的幾個問題

1、有大量的删除行,且已經送出,但是沒有被purge線程清理

這種情況由于大量删除行(或者update)并且已經送出,但是由于有長時間的select語句導緻read view記錄的狀态也比較陳舊,是以根據m_low_limit_no的判斷purge線程是不能清理一些比較老舊的undo的,是以這會導緻一個問題,如果這些del flag的記錄會存在于邏輯記錄連結清單内部,是以其他select掃描的時候回根據next offset掃描到,但是根據可見性判斷條件這些del flag的記錄trx id小于本select語句的read view 的 m_up_limit_id,是以是可見的debug如下:

387             return(view->changes_visible(trx_id, index->table->name));
(gdb) p view->changes_visible(trx_id, index->table->name)
$14 = true      

但是因為已經标記為del flag是以會做跳過處理如下:

row_search_mvcc:
 if (rec_get_deleted_flag(rec, comp)) {
  /* The record is delete-marked: we can skip it */
       ...
       goto next_rec;      

也就是實際上在長時間read view的“保護”下,我們的undo不能清理,并且del flag不能清理還儲存在block的邏輯連結清單中,掃描的時候會實際掃描到,隻是做了跳過處理。是以會出現如下現象

從一個案例深入剖析InnoDB隐式鎖和可見性判斷(1)

這就是上面說的原因,雖然沒有資料了,但是查詢依舊很慢。

2、大量删除,還未送出

那麼select掃描的時候會根據next offset 掃描到,但是由于read view 判斷這些資料的trx id 位于 m_up_limit_id和m_low_limit_id之間,需要根據事務是否活躍(read view的m_ids,顯然這裡是活躍的)通過undo建構其前印象,如下判斷:

lock_clust_rec_cons_read_sees
 trx_id_t trx_id = row_get_rec_trx_id(rec, index, offsets);
 return(view->changes_visible(trx_id, index->table->name));      
3、using index也可能回表

我們知道如果執行計劃使用到using index那麼不會回表去取主鍵的資料,使用整個二級索引即可。但是這裡有一種特殊情況,這裡進行描述。

對于二級索引而言,因為row記錄不包含trx id和undo ptr兩個僞列,那麼其可見性判斷和前的印象建構均需要回表擷取主鍵的記錄,當然可見性判斷可以先根據本二級索引page的max trx id是否小于read view的m_up_limit_id來進行第一次粗略過濾,那麼可見性判斷的可能性就低很多,如果通過了這個比對,那麼剩餘精确判斷還是需要回表通過主鍵來比對才行,如下:

  • 對于二級索引回表操作來講,精确的可見性判斷放到了回表後的lock_clust_rec_cons_read_sees函數上,關于二級索引的回表,參考附錄。
  • 對于不回表通路(using index),通過了粗略判斷後(lock_sec_rec_cons_read_sees),如果遇到需要精确的可見性判斷,那麼也是要回表的,原因前面解釋了(row記錄不包含trx id和undo ptr),參考附錄。

對于這個問題我們可以簡單的做如下的測試,當然需要打斷點才行:

測試表如下:
mysql> show create table testimp4 \G
*************************** 1. row ***************************
       Table: testimp4
Create Table: CREATE TABLE `testimp4` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) DEFAULT NULL,
  `b` int(11) DEFAULT NULL,
  `d` varchar(200) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `b` (`b`),
  KEY `d` (`d`)
) ENGINE=InnoDB AUTO_INCREMENT=10000 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from testimp4;
+------+------+------+------------------------------------+
| id   | a    | b    | d                                  |
+------+------+------+------------------------------------+
|    5 |    5 |  300 | NULL                               |
|    6 | 7000 | 7700 | 1124                               |
|   11 | 7000 | 7700 | 1124                               |
|   12 | 7000 | 7700 | 1124                               |
|   13 | 2900 | 1800 | NULL                               |
|   14 | 2900 | 1800 | NULL                               |
| 1000 |   88 | 1499 | NULL                               |
| 4000 | 6000 | 5904 | iiiafsafasfihhhccccchhhigggofgo111 |
| 4001 | 7000 | 7700 | 1124454555                         |
| 9999 | 9999 | 9999 | a                                  |
+------+------+------+------------------------------------+
10 rows in set (0.00 sec)      

對于下列語句的執行話是:

mysql> desc select b from testimp4  where b=300;
+----+-------------+----------+------------+------+---------------+------+---------+-------+------+----------+-------------+
| id | select_type | table    | partitions | type | possible_keys | key  | key_len | ref   | rows | filtered | Extra       |
+----+-------------+----------+------------+------+---------------+------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | testimp4 | NULL       | ref  | b             | b    | 5       | const |    1 |   100.00 | Using index |
+----+-------------+----------+------------+------+---------------+------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)      

我們做如下語句:

T1 T2
begin;delete from testimp4 where id=5;(不送出)
select b from testimp4 where b=300;(這裡是需要回表的)

這裡顯然T2(5 ,5 ,300 ,NULL )的這條記錄已經被T1删除了,但是沒有送出,T2首先判斷二級索引b上這行資料所在的page其max trx id是否小于本select語句的read view的m_up_limit_id,顯然這不成立,因為T1還會處于活躍狀态,然後就進入了回表判斷流程。棧如下:

#0  lock_clust_rec_cons_read_sees (rec=0x7fff060980a8 "\200", index=0x7ffec0499330, offsets=0x7fffe8399a70, view=0x33b1368)
    at /home/mysql/soft/percona-server-5.7.29-32/storage/innobase/lock/lock0lock.cc:369
#1  0x0000000001afbca4 in Row_sel_get_clust_rec_for_mysql::operator() (this=0x7fffe839a2d0, prebuilt=0x7ffec80c97a0, sec_index=0x7ffec049a2c0, rec=0x7fff060a008c "\200", 
    thr=0x7ffec80c9f88, out_rec=0x7fffe839a310, offsets=0x7fffe839a2e8, offset_heap=0x7fffe839a2f0, vrow=0x0, mtr=0x7fffe8399d90)
    at /home/mysql/soft/percona-server-5.7.29-32/storage/innobase/row/row0sel.cc:3763
#2  0x0000000001b00a94 in row_search_mvcc (buf=0x7ffec80c8a00 <incomplete sequence \375>, mode=PAGE_CUR_GE, prebuilt=0x7ffec80c97a0, match_mode=1, direction=0)
    at /home/mysql/soft/percona-server-5.7.29-32/storage/innobase/row/row0sel.cc:6051      

繼續閱讀