背景:
.
这是一个臭名昭彰的问题,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的读操作将不受影响