一、问题抛出
最近遇到一个问题,得到栈如下(5.6.25):
出现这个问题的时候只存在一个读写事务,那就是本事务。对这里的红色部分比较感兴趣,但是这里不是所有的内容都和这个问题相关,主要还是围绕可见性判断和隐式锁判定进行,算是我的思考过程。但是对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的逻辑链表中,扫描的时候会实际扫描到,只是做了跳过处理。因此会出现如下现象
这就是上面说的原因,虽然没有数据了,但是查询依旧很慢。
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还会处于活跃状态,然后就进入了回表判断流程。栈如下:
</div>