InnoDB 多版本控制 慢sql排查(基于MySQL 5.7)
一、版本控制
InnoDB是一个多版本存储引擎。它保留有关已更改行的旧版本的信息,以支持并发和回滚等事务功能。该信息存储在系统表空间或撤消表空间中称为回滚段的数据结构中。InnoDB 使用回滚段中的信息来执行事务回滚中所需的撤消操作。它还使用这些信息来构建行的早期版本以实现一致的读取。
什么是多版本存储引擎?
"多版本"这个概念主要是为了解决并发控制的问题。保存数据的多个版本,每个版本对应一个特定的用户操作。它允许读和写操作同时发生,而不需要进行读写锁定
在内部,InnoDB向数据库中存储的每一行添加三个字段:
- 6 字节DB_TRX_ID 标记更新当前数据记录的transaction id,每处理一个事务,其值自动+1。。此外,删除在内部被视为更新,其中设置行中的特殊位以将其标记为已删除。
- DB_ROLL_PTR 记录了最新一次修改该条记录的undo log,回滚的时候就通过这个指针找到undo log回滚。
- 6 字节DB_ROW_ID 当数据表没有指定主键时,数据库会自动以这个列来作为主键,生成聚集索引。
如何实现多版本存储引擎?
1、undo log链
每个事务都有自己的一个id,就像其身份证一样唯一标记该事务。当事务启动的时候,向Innodb存储引擎进行申请。假设此时如果打南边来了个事务A,它的事务id为12,事务A对表中的数据字段count进行修改,修改后该条数据对应的事务id为12,同时回滚指针指向实际的undo log回滚日志的地址。
此时打北边又来了个事务B,它的事务id为20,事务B将表中的数据字段count修改为21,对应数据的事务id变为20,回滚指针指向上一条undo log信息。如下图所示:
2、ReadView实现事务隔离
Mysql执行事务的时候,会生成一个ReadView,其中会包含以下重要信息:
(1)m_ids:mysql中未提交的事务id集合;
(2)min_trx_id:集合中最小的事务id;
(3)max_trx_id,mysql下一个要生成的事务id,也就是事务id集合中最大的事务id加1;
(4)当前需要执行的事务id;
假设现在有事务A以及事务B两个事务,A事务需要读取数据,B事务需要修改数据。
当事务A需要读取数据时,开启ReadView。由于此时数据库活跃的事务为事务A以及事务B,那么对应的ReadView中m_ids={12,34},min_trx_id=12,max_trx_id=35,当前需要执行的事务id为12。此时事务A读取数据时,先判断当前的事务id为12,而数据中的事务id为11,小于当前事务A的id。说明当前读取的数据是在事务A开启之前提交的,因此可以正常进行数据读取。
如果此时事务B进行了数据修改,修改count为29。而事务A再次进行数据读取时,继续进行判断,发现当前数据对应的事务id为34比当前的查询事务要大,但是小于max_trx_id,同时在m_ids中。说明该事务id对应的事务和事务A属于并发执行事务,因此不能进行数据读取。则根据undo log版本链,往上寻找undo log信息。如果找到的事务id小于当前读取数据的id则证明此时的数据是在当前开启查询事务之前提交的,因此可以进行数据的查询。
那么另外一个问题又来了,RC级别又是如何实现的呢?所谓RC级别,就是当别人的事务提交后,你就可以读取到别人修改后的值。因此会发生不可重复读问题。当设置为事务级别为RC时,它每次发起数据查询(set session transaction isolation level read committed;)后,每次进行数据查询都会新开启一个新的ReadView。
假设当前事务中活跃着两个事务,他们的事务id分别是12、34。此时事务id为34的事务更新了数据。此时数据更新为29。同时数据对应的事务id更新为34,同时回滚指针指向上一条数据。若此时事务id为15的事务进行数据查询,此时开启readview,进行检查,发现此时的数据中对应的事务id在活跃事务id中,说明是和查询事务差不多时机执行的,但是此时的事务还未提交。因此此时的数据不可以读,所以顺着undolog版本链条读取上一次的数据。同时进行判断。
如果事务B进行了提交,那么事务A再次进行数据查询的时候,会新开启一个ReadView,我们暂且称之为ReadViewA1,由于此时的事务B已经提交,所以ReadViewA1中对应的活跃列表中只有事务A对应的事务id为12。此时发现事务已提交,不再活跃事务列表中,因此可以进行数据读取。
回滚段中的撤消日志分为插入撤消日志和更新撤消日志。插入undo日志仅在事务回滚时需要,并且可以在事务提交后立即丢弃。更新撤消日志也用于一致读取,但只有在不存在已为其 InnoDB分配快照的事务之后,它们才能被丢弃
建议您定期提交事务,包括仅发出一致读取的事务。否则, InnoDB无法丢弃更新撤消日志中的数据,并且回滚段可能会变得太大,填满其所在的表空间。
如果您在表中以大约相同的速率插入和删除小批量的行,则清除线程可能会开始滞后,并且由于所有“死” 行,表可能会变得越来越大,从而 使所有内容都受磁盘限制并且非常严重。慢的。在这种情况下,请限制新行操作,并通过调整系统变量为清除线程分配更多资源 innodb_max_purge_lag。
在一个表中以大致相同的速率小批量地插入和删除行时,可能会导致“死”行(即被标记为删除但尚未被清除的行)积累,从而使表不断增大。这会导致磁盘成为瓶颈,使得整个系统变得非常缓慢。
在这种情况下,你可以通过调整innodb_max_purge_lag系统变量来控制新行操作的速度,并为清除线程分配更多的资源。例如,你可以降低innodb_max_purge_lag的值,使数据库能够更快地清除旧的“死”记录。同时,你也可以增加清除线程的数量或提高其优先级,以确保数据库能够更快地清除这些“死”记录。
二、多版本和二级索引
InnoDB多版本并发控制 (MVCC) 对待二级索引的方式与对待聚集索引的方式不同。聚集索引中的记录就地更新,它们的隐藏系统列(就是上面说的那三个隐藏字段)指向撤消日志条目,可以从中重建早期版本的记录。
与聚集索引记录不同,二级索引记录不包含隐藏的系统列,也不会就地更新。
当更新二级索引列时,旧的二级索引记录将被标记为删除,新记录将被插入,并最终清除标记为删除的记录。当更新二级索引列时,旧的二级索引记录将被标记为删除,新记录将被插入,并最终清除标记为删除的记录。当二级索引记录被删除标记或二级索引页被较新的事务更新时,InnoDB在聚集索引中查找数据库记录。在聚集索引中,将检查记录DB_TRX_ID,如果在启动读取事务后修改了记录,则从撤消日志中检索记录的正确版本
如果二级索引记录被标记为删除或者二级索引页被较新的事务更新, 则不使用覆盖索引技术。不是从索引结构返回值,而是InnoDB在聚集索引中查找记录。
但是,如果 启用了索引条件下推 (ICP) WHERE优化,并且可以仅使用索引中的字段来评估部分条件,则 MySQL 服务器仍会将这部分条件下推WHERE到存储引擎,并在存储引擎中使用指数。如果没有找到匹配的记录,则避免聚集索引查找。如果找到匹配的记录,即使在删除标记的记录中(如果部分条件已经在存储引擎层面得到评估,而且没有匹配的记录,那么 MySQL 可以直接返回结果,而无需进一步在聚集索引中查找。), InnoDB也会在聚集索引中查找该记录。
什么叫索引下推?
MySQL 的体系结构是分层的,数据库服务器处理查询请求,而存储引擎则负责处理数据的存储和检索。下推(Pushdown)是指将查询的一部分操作推送(下推)到存储引擎层级执行。具体来说,下推是将 WHERE 子句中的一些条件尽可能地在存储引擎层级上执行,而不是在 MySQL 服务器层级上执行。
三、补充
异常现象:
报慢sql,具体语句在图里
进行复现:
发现确实特别慢(关于第二次执行为什么会变快,放在buffer pool分享时候讲)
解析sql,发现走的是主键索引
在上线之前校验过,是可以走索引的,可能由于最近数据量的变化,mysql的优化策略变更导致不走二级索引
寻找原因:
在 MySQL 中,当执行带有 ORDER BY 子句的查询时,数据库引擎可能会选择不使用二级索引而进行全表扫描。
这是因为在包含排序的情况下,数据库引擎可能认为通过覆盖索引并不会带来额外的性能提升,而直接进行全表扫描可能更为高效。
解决办法:
办法一:指定执行索引
SELECT id, infoid, orderid, utel, stel, refusetime, reasoncd, reason, deleteflag, params, cityid, paidanid, cateid, baojieworkertype, `operator`, refusesource, utel_encrypt, stel_encrypt FROM t_app_refusereason FORCE INDEX (idx_refusetime_reasoncd_deleteflag_cityid) WHERE (cityid = 18 AND refusetime >= '2024-01-22 00:00:00' AND refusetime <= '2024-01-23 00:00:00' AND reasoncd IN (38, 52, 51, 10, 9) AND deleteflag = 1) ORDER BY id ASC LIMIT 50;
办法二:先按二级索引里字段排序,再按id排序
SELECT id,infoid,orderid,utel,stel,refusetime,reasoncd,reason,deleteflag,params,cityid,paidanid,cateid,baojieworkertype,`operator`,refusesource,utel_encrypt,stel_encrypt FROM t_app_refusereason WHERE ( cityid = 18 AND refusetime >= '2024-01-22 00:00:00' AND refusetime <= '2024-01-23 00:00:00' AND reasoncd IN ( 38, 52, 51, 10, 9 ) AND deleteflag = 1 ) ORDER BY refusetime ASC ,id ASC LIMIT 50;
办法三:取消排序
SELECT id,infoid,orderid,utel,stel,refusetime,reasoncd,reason,deleteflag,params,cityid,paidanid,cateid,baojieworkertype,`operator`,refusesource,utel_encrypt,stel_encrypt FROM t_app_refusereason WHERE ( cityid = 18 AND refusetime >= '2024-01-22 00:00:00' AND refusetime <= '2024-01-23 00:00:00' AND reasoncd IN ( 38, 52, 51, 10, 9 ) AND deleteflag = 1 ) LIMIT 50;
原sql、方案一、方案二、方案三
最终方案:
最终选择办法三,取消排序
复盘:
为什么之前要排序?
之前遇到一种场景,在查询es时没有排序,在返回结果的边界时会出现乱序。比如分页50条查询,数据A和B都符合筛选条件,查第一页的时候数据A返回在第50条,查第二页的时候第一条希望返回数据B,结果返回的还是数据A
因为es获取数据是在不同片区拿数据,如果不指定排序字段会出现不稳定排序现象。
但是mysql索引是稳定的,所以无特殊排序要求的时候直接拿就可以了