[MySQL5.7] Innodb的索引锁优化

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介:

背景:

.

这是一个臭名昭彰的问题,Innodb的btree发生合并/分裂等可能修改B-tree的操作时,都需要对其加排他的索引锁,这时候是无法对该索引进行读写操作的,极大的影响了性能;关于index lock,可以看看大神Domas的这篇博文: “Innodb locking makes me sad”  以及Vadim的这篇 博客
 .
总而言之,MySQL5.7.2的这个功能点的改进是万众期待的!
 .
以下是阅读 Rev6232的笔记,大概理了下关于索引锁优化的几个点。有些只是自己的理解,可能还需要仔细求证; 写的比较乱,同学们慎入- -!!
 .

1.新的读写锁类型:SX锁

 .
在之前的版本中,Innodb层有两种锁类型,一种是S共享锁,一种是X排他锁,在5.7.2增加了一种新的读写锁类型称为SX共享排他锁,这三类锁的关系为:

        | S|SX| X|

      –+–+–+–+
       S| o| o| x|
      –+–+–+–+
      SX| o| x| x|
      –+–+–+–+
       X| x| x| x|
      –+–+–+–+
 .
S锁和X锁与之前的逻辑相同,没有做变动
SX与SX和X互斥,与S共享,内部定义为RW_SX_LATCH,根据描述,在加上SX锁之后,不会影响读操作,但阻塞写操作
对应的加SX锁接口函数:rw_lock_sx_lock_func ,sx 的 lock_word取值是X lock的一半(X_LOCK_HALF_DECR).LOCK_WORD的范围代表的锁的含义见文件 sync/sync0rw.cc的注释
加锁逻辑比较简单,同样使用lock_word来判断,锁之间的冲突关系参阅函数rw_lock_sx_lock_low;加锁失败的话会去做spin,然后再retry,逻辑还是比较简单的。
 .
SX的定义感觉有点类似 seqlock,读写不阻塞,但写写阻塞;
 .
那么目前的逻辑中,有哪些情况要会用到SX锁呢,简单的玩了一下,大概有这么几个地方会用到:
#1
fsp_header_init:
    buf_page_get(space, zip_size, 0, RW_SX_LATCH, mtr);
当新create一个表时,会调用这个函数初始化space header,读入第0号page时使用SX lock
 .
fsp_header_init->fsp_fill_free_list:
1148                         /* We are going to initialize a new descriptor page
1149                         and a new ibuf bitmap page: the prior contents of the
1150                         pages should be ignored. */
1151
1152                         if (i > 0) {
1153                                 block = buf_page_create(
1154                                         space, i, zip_size, mtr);
1155                                 buf_page_get(space, zip_size, i,
1156                                              RW_SX_LATCH, mtr);
 .
>fsp_get_space_header
>fsp_alloc_seg_inode
>xdes_lst_get_descriptor
566         descr = fut_get_ptr(space, zip_size, lst_node, RW_SX_LATCH, mtr)
567                 – XDES_FLST_NODE;
>fseg_alloc_free_page_low
 .
看来innodb文件系统这块大量使用了SX锁
 .
>btr_root_get
根page加SX锁
 .
#2
dict_stats_analyze_index
在更新索引统计信息时,先给索引加个S锁获取btree的page数,然后在mtr_commit后,再加索引上的sx锁(mtr_sx_lock) ,随后计算索引上的统计信息
 .
#3
buf_flush_page:
当需要将page刷到磁盘时,加的是SX锁,这样在写磁盘的过程中,其他用户线程也可以读该block(但只对非压缩表生效)
 .
#4
btr_cur_search_to_nth_level:
779         switch (latch_mode) {
780         case BTR_MODIFY_TREE:
781                 /* Most of delete-intended operations are purging.
782                 Free blocks and read IO bandwidth should be prior
783                 for them, when the history list is glowing huge. */
784                 if (lock_intention == BTR_INTENTION_DELETE
785                     && trx_sys->rseg_history_len > BTR_CUR_FINE_HISTORY_LENGTH
786                     && buf_get_n_pending_read_ios()) {

787                         mtr_x_lock(dict_index_get_lock(index), mtr);
788                 } else {
789                         mtr_sx_lock(dict_index_get_lock(index), mtr);
790                 }
791                 upper_rw_latch = RW_X_LATCH;
792                 break;
当latch_mode为BTR_MODIFY_TREE时,需要对BTREE做操作,这里分两种情况:当purge操作处于比较繁忙的状态时,直接加索引X锁,退化到之前版本的状态,否则加SX锁,在修改BTREE时不阻塞读;
 .
在代码的注释中,有对设定退化的解释:
 .

/** For the index->lock scalability improvement, only possibility of clear

performance regression observed was caused by grown huge history list length.
That is because the exclusive use of index->lock also worked as reserving
free blocks and read IO bandwidth with priority. To avoid huge glowing history
list as same level with previous implementation, prioritizes pessimistic tree
operations by purge as the previous, when it seems to be growing huge.
 Experimentally, the history list length starts to affect to performance
throughput clearly from about 100000. */
#define BTR_CUR_FINE_HISTORY_LENGTH    100000
 .
 .
另外简单解释下,对于DML操作,总是先是乐观操作,即假定不会导致BTREE分裂/合并,如果判断乐观操作失败,则进行悲观操作,前者的LATCH_MODE类型为BTR_MODIFY_LEAF,后者为BTR_MODIFY_TREE (见函数row_ins_clust_index_entry)
BTR_MODIFY_TREE 不会使用ADAPTIVE HASH.
 .

2. BTREE操作意向类型(姑且这么翻译吧…)

 .
在MySQL5.7里增加了这么一组定义
 .
  83 enum btr_intention_t {
84         BTR_INTENTION_DELETE,
85         BTR_INTENTION_BOTH,
86         BTR_INTENTION_INSERT
87 };
 .
 .
根据latch_mode,调用函数btr_cur_get_and_clear_intention来设定。
 .
新增定义BTR_LATCH_FOR_INSERT和BTR_LATCH_FOR_DELETE,这两个宏决定了btr_intention的值,其作用是用于在需要修改BTREE时,优化块锁的范围;
 .
简单grep了一下代码,这两个标记是在做purge操作或者online ddl后的merge数据,或者在Undo回滚某些更改时会用到;我做的简单的插入操作则是BTR_INTENTION_BOTH
 .
在检索BTREE时, btr_intention可能会发生变化,以确保所有潜在会被修改的block被latch住。
 .
3.新增的latch_mode类型
几种latch mode,相比5.6,MySQL5.7.2增加了BTR_MODIFY_TREE和BTR_CONT_SEARCH_TREE
.
/** Latching modes for btr_cur_search_to_nth_level(). */
enum btr_latch_mode {
        /** Search a record on a leaf page and S-latch it. */
        BTR_SEARCH_LEAF = RW_S_LATCH,
        /** (Prepare to) modify a record on a leaf page and X-latch it. */
        BTR_MODIFY_LEAF = RW_X_LATCH,
        /** Obtain no latches. */
        BTR_NO_LATCHES = RW_NO_LATCH,
        /** Start modifying the entire B-tree. */
        BTR_MODIFY_TREE = 33,
        /** Continue modifying the entire B-tree. */
        BTR_CONT_MODIFY_TREE = 34,
        /** Search the previous record. */
        BTR_SEARCH_PREV = 35,
        /** Modify the previous record. */
        BTR_MODIFY_PREV = 36,
        /** Start searching the entire B-tree. */
        BTR_SEARCH_TREE = 37,
        /** Continue searching the entire B-tree. */
        BTR_CONT_SEARCH_TREE = 38
};
 .
对于BTR_SEARCH_TREE,只在需要扫描btree计算统计信息时需要用到,调用函数:
#dict_stats_analyze_index_level
#dict_stats_analyze_index_for_n_prefix
在使用BTR_SEARCH_TREE来搜索btree时,已经在btree上持有了S锁(BTR_ALREADY_S_LATCHED),该部分暂不展开;
 .
BTR_CONT_SEARCH_TREE 的使用封装在函数btr_page_get_father_node_ptr_for_validate中;
调用栈为btr_page_get_father_node_ptr_for_validate <- btr_validate_level <-btr_validate_index, 当执行类似CHECK TABLE这样的操作时会触发这部分逻辑。
 .

4.索引锁的优化逻辑

.
先简单的理一下MySQL5.6的btr_cur_search_to_nth_level函数逻辑,然后再看看5.7的改变;
 .

4.1 MySQL5.6的索引检索大概流程为:

 .
a.首先当latch mode为BTR_MODIFY_TREE 直接给BTREE加X latch, 对于BTR_CONT_MODIFY_TREE,需要保证X LATCH已经加好 ,其他latch_mode类型,则对索引加S LATCH
 .
b.设定 height = ULINT_UNDEFINED;  初始化检索,表示从根节点开始
 .
c.准备读page,设定将要读进buffer pool的page的latch类型;
当当前处于非叶子节点时,设定rw_latch为RW_NO_LATCH,表示不加latch;
当处于叶子节点时,对于BTR_MODIFY_LEAF,加RW_X_LATCH,对于BTR_SEARCH_LEAF,加RW_S_LATCH,并且会去判断当前的操作是否可以使用change buffer(ibuf_should_try)
 .
然后通过buf_page_get_gen读取需求的page ; (当可以使用change buffer时,如果page不在bp时,则调用change buffer的相关接口函数完成操作,直接返回)
这个page会根据之前设定的rw_latch加对应的latch类型;
 .
 .
d.如果当前读到的是叶子节点:
需要根据latch_mode来对Page加锁,具体见函数btr_cur_latch_leaves,加锁规则为:
BTR_SEARCH_LEAF:当前page加RW_S_LATCH
BTR_MODIFY_LEAF:当前page加RW_X_LATCH
BTR_MODIFY_TREE:当前page 及左右page,都需要加RW_X_LATCH
BTR_SEARCH_PREV:当前page及左节点加RW_S_LATCH
BTR_MODIFY_PREV: 当前page及左节点加RW_X_LATCH
 .
对于无需修改btree的操作,释放索引上的S锁(如果是在当前函数开始时锁上的索引S锁);
.
e.检查page内匹配的记录;
调用:

page_cur_search_with_match(

                block, index, tuple, page_mode, &up_match, &up_bytes,
                &low_match, &low_bytes, page_cursor);
在page内进行二分查找,找到对应的记录;暂时先不展开;
 .
 .
f.如果当前btree的level还没到达需求的层次:
准备检索下一层;height—
获取下一层的page no(btr_node_ptr_get_child_page_no)
返回c
 .
g.需求的level不是叶子节点,加X LATCH;对于叶子节点,还可能需要更新自适应哈希(btr_search_info_update)
 .

4.2. MySQL5.7的索引检索优化(只考虑MODIFY TREE的场景)

 .
#在purge带来的压力不大的情况下,modify tree操作只加SX锁,这意味着可以同时对索引进行读操作;
 .
#每往下层读一个page,都保存其savepoint到一个数组tree_savepoints中,读入的block维持在数组tree_blocks中;
 .
#在将page读入bp时,如果page还未到达目标层,读入时不加LATCH
 .
#对于叶子节点,还需要对当前page,及当前page的左右节点都加上X LATCH(btr_cur_latch_leaves)
 .
#如果当前检索的level还没有到达需求的层次:
>>如果当前的lock_intention是BTR_INTENTION_DELETE时,悲观删除操作,可能导致节点操作,释放所有占用的block的LATCH,将lock_intention更改为BTR_INTENTION_BOTH,并重新检索BTREE;
>>对于非唯一索引,如果当前匹配的node_ptr是page的第一个或者最后一个记录,或者最后一个/第一个记录的键值和node_ptr相匹配,将detected_same_key_root设置为TRUE;
这里没怎么看明白,根据注释,这种场景下如果latch mode为BTR_CONT_MODIFY_TREE,可能会选择到另外一个page,因此不可以释放节点的latch,避免死锁(阻塞另外一个检索相同key的线程)
 .
#当detected_same_key_root为false时,如果对当前page的操作可能导致潜在的btree修改(通过函数btr_cur_will_modify_tree判断,记录过少,或者page过满),则不可以释放上层的latch,如果不会导致潜在btree修改,则将当前page的上层page unpin掉(递减buf_fix_count),同时递增n_releases(从n_releases往前的在tree_blocks中记录的block都无需latch)
 .
#如果下一次要读的page到达目标level,对ROOT PAGE加SX LATCH,并将从n_releases到当前在tree_blocks数组中记录的block都X LATCH住(mtr_block_x_latch_at_savepoint)
 .
简而言之,当需要做悲观DML时,会对索引加SX锁,同时所有可能会导致btree修改的page都被X LATCH住,这样就将索引操作影响的范围限定在特定的page,其他page的读操作将不受影响

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
19天前
|
存储 缓存 关系型数据库
【MySQL进阶篇】存储引擎(MySQL体系结构、InnoDB、MyISAM、Memory区别及特点、存储引擎的选择方案)
MySQL的存储引擎是其核心组件之一,负责数据的存储、索引和检索。不同的存储引擎具有不同的功能和特性,可以根据业务需求 选择合适的引擎。本文详细介绍了MySQL体系结构、InnoDB、MyISAM、Memory区别及特点、存储引擎的选择方案。
【MySQL进阶篇】存储引擎(MySQL体系结构、InnoDB、MyISAM、Memory区别及特点、存储引擎的选择方案)
|
24天前
|
存储 关系型数据库 MySQL
MySQL存储引擎详述:InnoDB为何胜出?
MySQL 是最流行的开源关系型数据库之一,其存储引擎设计是其高效灵活的关键。InnoDB 作为默认存储引擎,支持事务、行级锁和外键约束,适用于高并发读写和数据完整性要求高的场景;而 MyISAM 不支持事务,适合读密集且对事务要求不高的应用。根据不同需求选择合适的存储引擎至关重要,官方推荐大多数场景使用 InnoDB。
67 7
|
1月前
|
存储 关系型数据库 MySQL
Mysql索引:深入理解InnoDb聚集索引与MyisAm非聚集索引
通过本文的介绍,希望您能深入理解InnoDB聚集索引与MyISAM非聚集索引的概念、结构和应用场景,从而在实际工作中灵活运用这些知识,优化数据库性能。
134 7
|
2月前
|
存储 Oracle 关系型数据库
【赵渝强老师】MySQL InnoDB的数据文件与重做日志文件
本文介绍了MySQL InnoDB存储引擎中的数据文件和重做日志文件。数据文件包括`.ibd`和`ibdata`文件,用于存放InnoDB数据和索引。重做日志文件(redo log)确保数据的可靠性和事务的持久性,其大小和路径可由相关参数配置。文章还提供了视频讲解和示例代码。
169 11
【赵渝强老师】MySQL InnoDB的数据文件与重做日志文件
|
1月前
|
存储 关系型数据库 MySQL
MySQL引擎InnoDB和MyISAM的区别?
InnoDB是MySQL默认的事务型存储引擎,支持事务、行级锁、MVCC、在线热备份等特性,主索引为聚簇索引,适用于高并发、高可靠性的场景。MyISAM设计简单,支持压缩表、空间索引,但不支持事务和行级锁,适合读多写少、不要求事务的场景。
61 9
|
2月前
|
存储 算法 关系型数据库
InnoDB与MyISAM实现索引方式的区别
InnoDB和MyISAM均采用B+树索引,但在实现上有所不同。InnoDB的主键索引在叶子节点存储完整数据记录,辅助索引则存储主键值;而MyISAM的主键索引与数据文件分离,仅存数据地址,且主辅索引无区别,支持非唯一主索引。
54 1
|
2月前
|
存储 Oracle 关系型数据库
【赵渝强老师】MySQL InnoDB的表空间
InnoDB是MySQL默认的存储引擎,主要由存储结构、内存结构和线程结构组成。其存储结构分为逻辑和物理两部分,逻辑存储结构包括表空间、段、区和页。表空间是InnoDB逻辑结构的最高层,所有数据都存放在其中。默认情况下,InnoDB有一个共享表空间ibdata1,用于存放撤销信息、系统事务信息等。启用参数`innodb_file_per_table`后,每张表的数据可以单独存放在一个表空间内,但撤销信息等仍存放在共享表空间中。
|
2月前
|
存储 Oracle 关系型数据库
【赵渝强老师】MySQL InnoDB的段、区和页
MySQL的InnoDB存储引擎逻辑存储结构与Oracle相似,包括表空间、段、区和页。表空间由段和页组成,段包括数据段、索引段等。区是1MB的连续空间,页是16KB的最小物理存储单位。InnoDB是面向行的存储引擎,每个页最多可存放7992行记录。
|
2月前
|
存储 Oracle 关系型数据库
【赵渝强老师】MySQL的InnoDB存储引擎
InnoDB是MySQL的默认存储引擎,广泛应用于互联网公司。它支持事务、行级锁、外键和高效处理大量数据。InnoDB的主要特性包括解决不可重复读和幻读问题、高并发度、B+树索引等。其存储结构分为逻辑和物理两部分,内存结构类似Oracle的SGA和PGA,线程结构包括主线程、I/O线程和其他辅助线程。
【赵渝强老师】MySQL的InnoDB存储引擎
|
7月前
|
存储 关系型数据库 MySQL
MySQL数据库进阶第六篇(InnoDB引擎架构,事务原理,MVCC)
MySQL数据库进阶第六篇(InnoDB引擎架构,事务原理,MVCC)