一、問題抛出
最近遇到一個問題,得到棧如下(5.6.25):
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5CM5Q2Y5gDO2MWMxIjZzMWO5czM0E2NzYzMjJjM2IDOl9CX5d2bs92Yl1iclB3bsVmdlR2LcNWaw9CXt92Yu4GZjlGbh5yYjV3Lc9CX6MHc0RHaiojIsJye.png)
二、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的邏輯連結清單中,掃描的時候會實際掃描到,隻是做了跳過處理。是以會出現如下現象
這就是上面說的原因,雖然沒有資料了,但是查詢依舊很慢。
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