从一个案例深入剖析InnoDB隐式锁和可见性判断(1)

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
云数据库 RDS PostgreSQL,高可用系列 2核4GB
简介: 从一个案例深入剖析InnoDB隐式锁和可见性判断

一、问题抛出

最近遇到一个问题,得到栈如下(5.6.25):

image.png

出现这个问题的时候只存在一个读写事务,那就是本事务。对这里的红色部分比较感兴趣,但是这里不是所有的内容都和这个问题相关,主要还是围绕可见性判断和隐式锁判定进行,算是我的思考过程。但是对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的逻辑链表中,扫描的时候会实际扫描到,只是做了跳过处理。因此会出现如下现象

image.png

这就是上面说的原因,虽然没有数据了,但是查询依旧很慢。

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



            </div>
相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
相关文章
|
2天前
|
人工智能 运维 安全
|
4天前
|
SpringCloudAlibaba 负载均衡 Dubbo
微服务架构下Feign和Dubbo的性能大比拼,到底鹿死谁手?
本文对比分析了SpringCloudAlibaba框架下Feign与Dubbo的服务调用性能及差异。Feign基于HTTP协议,使用简单,适合轻量级微服务架构;Dubbo采用RPC通信,性能更优,支持丰富的服务治理功能。通过实际测试,Dubbo在调用性能、负载均衡和服务发现方面表现更出色。两者各有适用场景,可根据项目需求灵活选择。
383 124
微服务架构下Feign和Dubbo的性能大比拼,到底鹿死谁手?
|
7天前
|
人工智能 JavaScript 测试技术
Qwen3-Coder入门教程|10分钟搞定安装配置
Qwen3-Coder 挑战赛简介:无论你是编程小白还是办公达人,都能通过本教程快速上手 Qwen-Code CLI,利用 AI 轻松实现代码编写、文档处理等任务。内容涵盖 API 配置、CLI 安装及多种实用案例,助你提升效率,体验智能编码的乐趣。
670 107
|
1天前
|
算法 Python
【轴承故障诊断】一种用于轴承故障诊断的稀疏贝叶斯学习(SBL),两种群稀疏学习算法来提取故障脉冲,第一种仅利用故障脉冲的群稀疏性,第二种则利用故障脉冲的额外周期性行为(Matlab代码实现)
【轴承故障诊断】一种用于轴承故障诊断的稀疏贝叶斯学习(SBL),两种群稀疏学习算法来提取故障脉冲,第一种仅利用故障脉冲的群稀疏性,第二种则利用故障脉冲的额外周期性行为(Matlab代码实现)
221 152
|
3天前
|
Java 数据库 数据安全/隐私保护
Spring 微服务和多租户:处理多个客户端
本文介绍了如何在 Spring Boot 微服务架构中实现多租户。多租户允许单个应用实例为多个客户提供独立服务,尤其适用于 SaaS 应用。文章探讨了多租户的类型、优势与挑战,并详细说明了如何通过 Spring Boot 的灵活配置实现租户隔离、动态租户管理及数据源路由,同时确保数据安全与系统可扩展性。结合微服务的优势,开发者可以构建高效、可维护的多租户系统。
200 127
|
3天前
|
Web App开发 前端开发 API
在折叠屏应用中,如何处理不同屏幕尺寸和设备类型的样式兼容性?
在折叠屏应用中,如何处理不同屏幕尺寸和设备类型的样式兼容性?
230 124
|
1天前
|
编解码 算法 自动驾驶
【雷达通信】用于集成传感和通信的OFDM雷达传感算法(Matlab代码实现)
【雷达通信】用于集成传感和通信的OFDM雷达传感算法(Matlab代码实现)
172 125
|
1天前
|
JavaScript 关系型数据库 MySQL
基于python的网上外卖订餐系统
本系统基于Python与Flask框架,结合MySQL数据库及Vue前端技术,实现了一个功能完善的网上订餐平台。系统涵盖餐品、订单、用户及评价管理模块,并深入研究订餐系统的商业模式、用户行为与服务质量。技术上采用HTML、PyCharm开发工具,支持移动端访问,助力餐饮业数字化转型。