MySQL · 引擎特性 · InnoDB Adaptive hash index介绍

一 序

先看官网上的介绍(翻译来自MK提丰 )

The adaptive hash index (AHI) lets InnoDB perform more like an in-memory database on systems with appropriate combinations of workload and ample memory for the buffer pool, without sacrificing any transactional features or reliability. This feature is enabled by the innodb_adaptive_hash_index option, or turned off by --skip-innodb_adaptive_hash_index at server startup.

     自适应哈希索引(AHI)使InnoDB平台看起来更像一个内存数据库(在系统负载适当并且分配给缓存池的内存充裕的情况下),且不牺牲任何事务特性或可靠性。这个特性可以在服务启动时通过innodb_adaptive_hash_index选项生效,或者通过--skip-innodb_adaptive_hash_index关闭。

Based on the observed pattern of searches, MySQL builds a hash index using a prefix of the index key. The prefix of the key can be any length, and it may be that only some of the values in the B-tree appear in the hash index. Hash indexes are built on demand for those pages of the index that are often accessed.

      根据所观察到的的搜索模式,MySQL用一个索引键的前缀建立一个哈希索引。该键的前缀可以是任意长度,并且只有B-tree中的少数值才会出现在哈希索引中。哈希索引是为了获取那些被频繁访问的索引页而建立的。

If a table fits almost entirely in main memory, a hash index can speed up queries by enabling direct lookup of any element, turning the index value into a sort of pointer. InnoDB has a mechanism that monitors index searches. If InnoDB notices that queries could benefit from building a hash index, it does so automatically.

      如果一张表几乎整个被至于主内存中,那么有了哈希索引就可以直接查找任意元素并将其索引值转换为一系列的指针从而提升查询速度。InnoDB有一个机制可以监控索引的搜索。如果InnoDB注意到查询可以通过建立哈希索引得到优化,那么他就会自动做这件事。

With some workloads, the speedup from hash index lookups greatly outweighs the extra work to monitor index lookups and maintain the hash index structure. Sometimes, the read/write lock that guards access to the adaptive hash index can become a source of contention under heavy workloads, such as multiple concurrent joins. Queries with LIKE operators and % wildcards also tend not to benefit from the AHI. For workloads where the adaptive hash index is not needed, turning it off reduces unnecessary performance overhead. Because it is difficult to predict in advance whether this feature is appropriate for a particular system, consider running benchmarks with it both enabled and disabled, using a realistic workload. The architectural changes in MySQL 5.6 and higher make more workloads suitable for disabling the adaptive hash index than in earlier releases, although it is still enabled by default.

       只需增加少量负载,这种由于哈希索引查询所带来的速度提升大大超过监控索引查询和维护哈希索引结构的所带来的额外工作量。有时,在高负载的情况下守护自适应哈希索引访问的读写锁会变成一种竞争资源,例如多重并发关联。基于LIKE操作和%通配符的查询也往往不通过AHI来优化。对于自适应哈希索引所不需要的负载,关闭它以节省不必要的性能开销。由于难以提前预测该特性是否适用于某一特定系统,需要在实际负载下,识别在其启用和禁用时的运行指标。该架构在MySQL 5.6及以上版本中改变,相比之前的版本,禁用自适应哈希索引会适当的产生更多的负载。

In MySQL 5.7, the adaptive hash index search system is partitioned. Each index is bound to a specific partition, and each partition is protected by a separate latch. Partitioning is controlled by the innodb_adaptive_hash_index_parts configuration option. In earlier releases, the adaptive hash index search system was protected by a single latch which could become a point of contention under heavy workloads. The innodb_adaptive_hash_index_parts option is set to 8 by default. The maximum setting is 512.

      从MySQL 5.7开始,自适应哈希索引搜索系统是分区的。每个索引都会绑定到一个特殊的分区上,并且每个分区都由各自独立的锁存器来保护。分区受到innodb_adaptive_hash_index_parts配置项的控制。在MySQL5.7之前,自适应哈希索引搜索系统是通过一个单独的锁存器来保护,在高负载的情况下它会变成竞争点。innodb_adaptive_hash_index_parts选项默认值为8,最大值为512。

The hash index is always built based on an existing B-tree index on the table. InnoDB can build a hash index on a prefix of any length of the key defined for the B-tree, depending on the pattern of searches that InnoDB observes for the B-tree index. A hash index can be partial, covering only those pages of the index that are often accessed.

      哈希索引的建立总是基于表上的一个现有的B-tree索引。InnoDB可以用任意长度的B-tree键的前缀建索引,依赖于InnoDB从B-tree索引上所观察到的搜索模式。哈希索引可以是局部的,仅覆盖哪些经常被访问的索引页。

You can monitor the use of the adaptive hash index and the contention for its use in the SEMAPHORES section of the output of theSHOW ENGINE INNODB STATUS command. If you see many threads waiting on an RW-latch created in btr0sea.c, then it might be useful to disable adaptive hash indexing.

     你可以通过SHOW ENGINE INNODB STATUS命令所输出的SEMAPHORES部分来监控自适应哈希索引的使用及其竞争情况。如果你看到许多线程正在等待一个在btr0sea.c中创建的RW-latch,然后它可能被用于禁用自适应哈希索引。

For more information about the performance characteristics of hash indexes, see Section 8.3.8, “Comparison of B-Tree and Hash Indexes”.

更多信息关于哈希索引的性能特性,请参考8.3.8,“B-Tree和哈希索引的竞争”。

二 AHI作用

查询过程

   row_search_mvcc mysql5.7源码在innobase/row/row0sel.cc。如果是mysql5.6版本row_search_for_mysql

   这源码很长,截取了其中的注释:

  /* PHASE 0: Release a possible s-latch we are holding on the     adaptive hash index latch if there is someone waiting behind 

  如果发现其它线程需要对btr_search_latch上锁,则释放 btr_search_latch,然后执行 1

/* PHASE 1: Try to pop the row from the prefetch cache */

  1尝试从 row_prebuilt_t->fetch_cache 中取数据库记录,有则直接返回,如果没有数据或者不可以使用 fetch cache, 则执行2

/* PHASE 2: Try fast adaptive hash index search if possible */

2在满足条件的情况下,使用 AHI 定位 cursor 位置并返回数据, 否则执行 3

row_sel_try_search_shortcut_for_mysql

/* PHASE 3: Open or restore index cursor position */

3. 打开并恢复索引的游标位置

/* PHASE 4: Look for matching records in a loop */

.根据查找的值在叶子结点中逐个匹配,查找满足条件的记录,返回数据,取下一条记录时执行 3,5

/* PHASE 5: Move the cursor to the next index record */

  5.移动 cursor 到下一条记录并返回数据

****************************************************

即使读了taobao.mysql的文章,还是没找到源码对应的入口:btr_cur_search_to_nth_level

看注释是AHI 则在第 [2, 3] 两个步骤中影响着定位叶子结点的过程,根据查询条件定位叶子节点的过程中发挥着 hash 的作用。

看看淘宝对此的解释吧:

     我们知道InnoDB的索引组织结构为Btree。通常情况下,我们需要根据查询条件,从根节点开始寻路到叶子节点,找到满足条件的记录。为了减少寻路开销,InnoDB本身做了几点优化。

  • 首先,对于连续记录扫描(prebuilt->n_rows_fetched >= MYSQL_FETCH_CACHE_THRESHOLD),InnoDB在满足比较严格的条件时采用row cache的方式连续读取MYSQL_FETCH_CACHE_SIZE8条记录(并将记录格式转换成MySQL Format),存储在线程私有的row_prebuilt_t::fetch_cache中;这样一次寻路就可以获取多条记录,在server层处理完一条记录后,可以直接从cache中取数据而无需再次寻路,直到cache中数据取完,再进行下一轮。
  • 另一种方式是,当一次进入InnoDB层获得数据后,在返回server层前,当前在btree上的cursor会被暂时存储到row_prebuilt_t::pcur中,当再次返回InnoDB层捞数据时,如果对应的Block没有发生任何修改,则可以继续沿用之前存储的cursor,无需重新定位。

      由以上的分析可以看到 MySQL 一次定位 cursor 的过程即是从根结点到叶子结点的路径,时间复杂度为:height(index) + [CPU cost time],上述的两个优化过程无法省略定位 cursor 的中间结点,因此需要引入一种可以从 search info 定位到叶子结点的方法,从而省略根结点到叶子结点的路径上所消耗的时间,而这种方法即是 自适应索引(Adaptive hash index, AHI),AHI是一个内存结构,严格来说不是传统意义上的索引,可以把它理解为建立在Btree索引上的“索引”。

      AHI 的实现主要包括 AHI 初始化过程、构建条件、使用过程、维护过程、系统监控等部分,我们从源码的实现的角度上分析上述过程。源码版本为5.7.18.

三 初始化

     AHI在内存中表现就是一个普通的哈希表对象,存储在btr_search_sys_t::hash_index中。在系统启动的时候会随着 buffer_pool 的初始化而自动的建立相应的内存结构,其初始化过程为:

  • 利用系统内存 (malloc) 创建全局变量 btr_search_sys 及其锁结构
  • 利用系统内存 (malloc) 建立 hash_table 内存结构,并初始化其成员变量,其中 hash_table 数组的大小取决于当前 buffer_pool 的 size 与 系统的机器位数,计算公式为:buf_pool_get_curr_size() / sizeof(void*) / 64

参考函数:btr_search_sys_create,源码如下,在/innobase/btr/btr0sea.cc

/** Creates and initializes the adaptive search system at a database start.
@param[in]	hash_size	hash table size. */
void
btr_search_sys_create(ulint hash_size)
{
	/* Search System is divided into n parts.
	Each part controls access to distinct set of hash buckets from
	hash table through its own latch. */

	/* Step-1: Allocate latches (1 per part). */
	btr_search_latches = reinterpret_cast<rw_lock_t**>(
		ut_malloc(sizeof(rw_lock_t*) * btr_ahi_parts, mem_key_ahi));

	for (ulint i = 0; i < btr_ahi_parts; ++i) {

		btr_search_latches[i] = reinterpret_cast<rw_lock_t*>(
			ut_malloc(sizeof(rw_lock_t), mem_key_ahi));

		rw_lock_create(btr_search_latch_key,
			       btr_search_latches[i], SYNC_SEARCH_SYS);
	}

	/* Step-2: Allocate hash tablees. */
	btr_search_sys = reinterpret_cast<btr_search_sys_t*>(
		ut_malloc(sizeof(btr_search_sys_t), mem_key_ahi));

	btr_search_sys->hash_tables = reinterpret_cast<hash_table_t**>(
		ut_malloc(sizeof(hash_table_t*) * btr_ahi_parts, mem_key_ahi));

	for (ulint i = 0; i < btr_ahi_parts; ++i) {

		btr_search_sys->hash_tables[i] =
			ib_create((hash_size / btr_ahi_parts),
				  LATCH_ID_HASH_TABLE_MUTEX,
				  0, MEM_HEAP_FOR_BTR_SEARCH);

#if defined UNIV_AHI_DEBUG || defined UNIV_DEBUG
		btr_search_sys->hash_tables[i]->adaptive = TRUE;
#endif /* UNIV_AHI_DEBUG || UNIV_DEBUG */
	}
}
  • MySQL 5.7已经开始支持InnoDB buffer pool的动态调整,其策略是buffer pool的大小改变超过1倍,就重新分配AHI Hash内存(btr_search_sys_resize)。
  • 所有 buffer_pool instances 共享一个 AHI, 而不是每一个 buffer_pool instance 一个 AHI
  • 5.7.8 之前 AHI 只有一个全局的锁结构 btr_search_latch, 当压力比较大的时候会出现性能瓶颈,5.7.8 对 AHI 进行了拆锁处理,详情可以参考函数: btr_get_search_table() & btr_search_sys_create()
  • AHI 的 btr_search_latch (bug#62018) & index lock 是MySQL中两个比较大的锁,详情可以参考 Index lock and adaptive search – next two biggest InnoDB problems,5.7 通过对 AHI 锁拆分 (5.7 commit id: ab17ab91) 以及引入不同的索引锁协议 (WL#6326) 解决了这两个问题。

四 AHI的实现解析

4.1 触发AHI信息统计

          AHI 是建立在 search info & REC 内存地址之间的映射信息,在系统接受访问之前并没有足够的信息来建立 AHI 的映射信息,所以需要搜集 SQL 语句在执行过程中的 search_info & block info 信息并判断是否可以为数据页建立 AHI 缓存,其中:

search info 对应 btr_search_t, 用于记录 index 中的 n_fields (前缀索引列数) & n_bytes(last column bytes) 信息,这些被用于计算 fold 值;

block info 用于记录计算 fold 的值所需要的 fields & bytes 之外,还记录了在此情况下使用 AHI 在此数据页上潜在成功的次数;

在第一次执行SQL时,需要从btree的root节点开始,当寻址到匹配的叶子节点时,会走如下逻辑
btr_cur_search_to_nth_level(按照上面的逻辑,定位 cursor 的过程中,但是我从没找到调用)

if (btr_search_enabled && !index->disable_ahi) {
        btr_search_info_update(index, cursor);
}
btr_search_info_update(
/*===================*/
	dict_index_t*	index,	/*!< in: index of the cursor */
	btr_cur_t*	cursor)	/*!< in: cursor which was just positioned */
{
	ut_ad(!rw_lock_own(btr_get_search_latch(index), RW_LOCK_S));
	ut_ad(!rw_lock_own(btr_get_search_latch(index), RW_LOCK_X));

	if (dict_index_is_spatial(index) || !btr_search_enabled) {
		return;
	}

	btr_search_t*	info;
	info = btr_search_get_info(index);

	info->hash_analysis++;

	if (info->hash_analysis < BTR_SEARCH_HASH_ANALYSIS) {

		/* Do nothing */

		return;

	}

	ut_ad(cursor->flag != BTR_CUR_HASH);

	btr_search_info_update_slow(info, cursor);
}

     这里会判断脏读AHI开关(btr_search_enabled)是否打开,以及index->diable_ahi是否为false。第二个条件是MySQL5.7对临时表的优化,避免临时表操作对全局对象的影响,针对临时表不做AHI构建。

    我们看看函数btr_search_info_update的逻辑:

  1. info->hash_analysis++,当info->hash_analysis值超过BTR_SEARCH_HASH_ANALYSIS(17)时,也就是说对该索引寻路到叶子节点17次后,才会去做AHI分析(进入步骤2)
  2. 进入函数btr_search_info_update_slow

     在连续执行17次对相同索引的操作后,满足info->hash_analysis大于等于BTR_SEARCH_HASH_ANALYSIS的条件,就会调用函数btr_search_info_update_slow来更新search_info,这主要是为了避免频繁的索引查询分析产生的过多CPU开销。

   btr_search_info_update_slow包含三个部分:更新索引查询信息、block上的查询信息以及为当前block构建AHI,下面几小节分别介绍。

4.2更新索引上的查询信息

参考函数:btr_search_info_update_hash  源码在/innobase/btr/btr0sea.cc

       这里涉及到的几个search_info变量包括: btr_search_t::n_hash_potential 表示如果使用AHI构建索引,潜在的可能成功的次数; btr_search_t::hash_analysis  若设置了新的建议前缀索引模式,则重置为0,随后的17次查询分析可以忽略更新search_info。

     下面两个字段表示推荐的前缀索引模式: btr_search_t::n_fields 推荐构建AHI的索引列数; btr_search_t::left_side 表示是否在相同索引前缀的最左索引记录构建AHI;值为true时,则对于相同前缀索引的记录,只存储最右的那个记录。 通过n_fields和left_side可以指导选择哪些列作为索引前缀来构建(fold, rec)哈希记录。如果用户的SQL的索引前缀列的个数大于等于构建AHI时的前缀索引,就可以用上AHI。
两种情况需要构建建议的前缀索引列:
当前是第一次为该索引做AHI分析,btr_search_t::n_hash_potential ==0,需要构建建议的前缀索引列;
新的记录匹配模式发生了变化(info->left_side == (info->n_fields <=cursor->low_match)),需要重新设置前缀索引列。

我看到这里不太理解,找了下源码看

if (info->n_hash_potential == 0) {  

		goto set_new_recomm;  //构建建议的前缀索引列
	}

	/* Test if the search would have succeeded using the recommended
	hash prefix */

	if (info->n_fields >= n_unique && cursor->up_match >= n_unique) {
increment_potential:
		info->n_hash_potential++;

		return;
	}

	cmp = ut_pair_cmp(info->n_fields, info->n_bytes,
			  cursor->low_match, cursor->low_bytes);

	if (info->left_side ? cmp <= 0 : cmp > 0) {

		goto set_new_recomm; //构建建议的前缀索引列
	}

  后来的代码做了优化,抽取了ut_pair_cmp函数。 可以结合函数去理解《=0,就是ah(info->n_fields)<=b_h(cursor->low_match)

ut_pair_cmp(
    ulint    a_h,
    ulint    a_l,
    ulint    b_h,
    ulint    b_l)
{
    if (a_h < b_h) {
        return(-1);
    }
    if (a_h > b_h) {
        return(1);
    }
    return(ut_ulint_cmp(a_l, b_l));
}

构建建议的前缀索引列代码如下:

set_new_recomm:
	/* We have to set a new recommendation; skip the hash analysis
	for a while to avoid unnecessary CPU time usage when there is no
	chance for success */

	info->hash_analysis = 0;

	cmp = ut_pair_cmp(cursor->up_match, cursor->up_bytes,
			  cursor->low_match, cursor->low_bytes);
	if (cmp == 0) {
		info->n_hash_potential = 0;

		/* For extra safety, we set some sensible values here */

		info->n_fields = 1;
		info->n_bytes = 0;

		info->left_side = TRUE;

	} else if (cmp > 0) {
		info->n_hash_potential = 1;

		if (cursor->up_match >= n_unique) {

			info->n_fields = n_unique;
			info->n_bytes = 0;

		} else if (cursor->low_match < cursor->up_match) {

			info->n_fields = cursor->low_match + 1;
			info->n_bytes = 0;
		} else {
			info->n_fields = cursor->low_match;
			info->n_bytes = cursor->low_bytes + 1;
		}

		info->left_side = TRUE;
	} else {
		info->n_hash_potential = 1;

		if (cursor->low_match >= n_unique) {

			info->n_fields = n_unique;
			info->n_bytes = 0;
		} else if (cursor->low_match > cursor->up_match) {

			info->n_fields = cursor->up_match + 1;
			info->n_bytes = 0;
		} else {
			info->n_fields = cursor->up_match;
			info->n_bytes = cursor->up_bytes + 1;
		}

		info->left_side = FALSE;
	}

从上述代码可以看到,在low_match和up_match之间,选择小一点match的索引列数的来进行设置,但不超过唯一确定索引记录值的列的个数:

  • 当low_match小于up_match时,left_side设置为true,表示相同前缀索引的记录只缓存最左记录;
  • 当low_match大于up_match时,left_side设置为false,表示相同前缀索引的记录只缓存最右记录。

如果不是第一次进入seach_info分析,有两种情况会递增btr_search_t::n_hash_potential:

  • 本次查询的up_match和当前推荐的前缀索引都能唯一决定一条索引记录(例如唯一索引),则根据search_info推荐的前缀索引列构建AHI肯定能命中,递增 info->n_hash_potential;
/* Test if the search would have succeeded using the recommended
	hash prefix */

	if (info->n_fields >= n_unique && cursor->up_match >= n_unique) {
increment_potential:
		info->n_hash_potential++;

		return;
	}
  • 本次查询的tuple可以通过建议的前缀索引列构建的AHI定位到。info->left_side == (info->n_fields <= cursor->up_match)
cmp = ut_pair_cmp(info->n_fields, info->n_bytes,
			  cursor->up_match, cursor->up_bytes);

	if (info->left_side ? cmp <= 0 : cmp > 0) {

		goto increment_potential;
	}

     很显然,如果对同一个索引的查询交替使用不同的查询模式,可能上次更新的search_info很快就会被重新设置,具有固定模式的索引查询将会受益于AHI索引。

     补充知识:btr_cur_search_to_nth_level 中在定位 cursor 的过程中会在树的每一层调用 page_cur_search_with_match 来确定下一个 branch 结点或叶子结点,page_cur_search_with_match 函数会将查询过程中比较的前缀索引列数 & 最后一列匹配的字节数记录至 {cursor->up_match, cursor->up_bytes, cursor->low_bytes, cursor->low_match},目的是为了保存与 search tuple 在比较过程时的最小比较单元,详细的计算过程可以参考 page_cur_search_with_match 的实现代码。源码在innobase/page/page0cur.cc。

    4.3 更新block上的查询信息

     参考函数:btr_search_update_block_hash_info 源码在innobase/btr/btr0sea.cc这个也是btr_search_info_update_slow调用的。

    更新数据页block上的查询信息,涉及到修改的变量包括:btr_search_info::last_hash_succ 最近一次成功(或可能成功)使用AHI; buf_block_t::n_hash_helps 计数值,如果使用当前推荐的前缀索引列构建AHI可能命中的次数,用于启发构建/重新构建数据页上的AHI记录项; buf_block_t::n_fields 推荐在block上构建AHI的前缀索引列数; buf_block_t::left_side 和search_info上对应字段含义相同。

函数主要流程包括:

  1. 首先设置btr_search_info::last_hash_succ 为FALSE 这会导致在分析过程中无法使用AHI进行检索。
  2. 初始化或更新block上的查询信息
if ((block->n_hash_helps > 0)
	    && (info->n_hash_potential > 0)
	    && (block->n_fields == info->n_fields)
	    && (block->n_bytes == info->n_bytes)
	    && (block->left_side == info->left_side)) {

		if ((block->index)
		    && (block->curr_n_fields == info->n_fields)
		    && (block->curr_n_bytes == info->n_bytes)
		    && (block->curr_left_side == info->left_side)) {

			/* The search would presumably have succeeded using
			the hash index */

			info->last_hash_succ = TRUE;
		}

		block->n_hash_helps++;
	} else {
		block->n_hash_helps = 1;
		block->n_fields = info->n_fields;
		block->n_bytes = info->n_bytes;
		block->left_side = info->left_side;
	}

       当block第一次被touch到并进入该函数时,设置block上的建议索引列值;以后再进入时,如果和索引上的全局search_info相匹配,则递增block->n_hash_helps,启发后续的创建或重构建AHI。
       如果当前数据页block上已经构建了AHI记录项,且buf_block_t::curr_n_fields等字段和btr_search_info上对应字段值相同时,则认为当前SQL如果使用AHI索引能够命中,因此将btr_search_info::last_hash_succ设置为true,下次再使用相同索引检索btree时就会尝试使用AHI。

    如果 index->search_info 的匹配格式 & 该数据页上保存的匹配模式不相同,则设置 block->n_hash_helps=1 且使用 index->search_info 对 block 上的索引匹配信息进行重新设置,详细过程可参考 btr_search_update_block_hash_info

 3  在初始化或更新block上的变量后,需要判断是否为整个page构建AHI索引:

if ((block->n_hash_helps > page_get_n_recs(block->frame)
	     / BTR_SEARCH_PAGE_BUILD_LIMIT)
	    && (info->n_hash_potential >= BTR_SEARCH_BUILD_LIMIT)) {

		if ((!block->index)
		    || (block->n_hash_helps
			> 2 * page_get_n_recs(block->frame))
		    || (block->n_fields != block->curr_n_fields)
		    || (block->n_bytes != block->curr_n_bytes)
		    || (block->left_side != block->curr_left_side)) {

			/* Build a new hash index on the page */

			return(TRUE);
		}
	}

  简单来说,当满足下面三个条件时,就会去为整个block上构建AHI记录项:

  • 分析使用AHI可以成功查询的次数(buf_block_t::n_hash_helps)超过block上记录数的16(BTR_SEARCH_PAGE_BUILD_LIMIT)分之一;
  • btr_search_info::n_hash_potential大于等于BTR_SEARCH_BUILD_LIMIT (100),表示连续100次潜在的成功使用AHI可能性;
  • 尚未为当前block构造过索引、或者当前block上已经构建了AHI索引且block->n_hash_helps大于page上记录数的两倍、或者当前block上推荐的前缀索引列发生了变化 。

4.4为数据页构建AHI索引

     如果在上一阶段判断认为可以为当前page构建AHI索引(函数btr_search_update_block_hash_info返回值为TRUE),则根据当前推荐的索引前缀进行AHI构建。

   参考函数:btr_search_build_page_hash_index  源码在innobase/btr/btr0sea.cc,也是btr_search_info_update_slow调用。 

分为三个阶段:
检查阶段:加btr_search_latch的S锁,判断AHI开关是否打开;如果block上已经构建了老的AHI但前缀索引列和当前推荐的不同,则清空Block对应的AHI记录项(btr_search_drop_page_hash_index);检查n_fields和page上的记录数;然后释放btr_search_latch的S锁;

if (block->index && ((block->curr_n_fields != n_fields)
			     || (block->curr_n_bytes != n_bytes)
			     || (block->curr_left_side != left_side))) {

		btr_search_s_unlock(index);

		btr_search_drop_page_hash_index(block);
	} else {
		btr_search_s_unlock(index);
	}

搜集阶段:根据推荐的索引列数计算记录fold值,将对应的数据页记录内存地址到数组里;

调用 btr_search_check_free_space_in_heap 来确保 AHI 有足够的内存生成映射信息 ha_node_t {fold, data, next},该内存从 buffer_pool->free 链表获得,详情参考:buf_block_alloc(), fold 的值的计算可参考函数:rec_fold();

插入阶段:加btr_search_latch的X锁,将第二阶段搜集的(fold, rec)插入到AHI中,并更新:


	/* This counter is decremented every time we drop page
	hash index entries and is incremented here. Since we can
	rebuild hash index for a page that is already hashed, we
	have to take care not to increment the counter in that
	case. */
	if (!block->index) {
		assert_block_ahi_empty(block);
		index->search_info->ref_count++;
	}

	block->n_hash_helps = 0;

	block->curr_n_fields = n_fields;
	block->curr_n_bytes = n_bytes;
	block->curr_left_side = left_side;
	block->index = index;

     由于操作过程中释放了 btr_search_latch,需要再次检查 block 上的AHI信息是否发生了变化,如果发生变化则退出函数;
    调用 ha_insert_for_fold 方法将之前收集的信息生成 ha_node_t, 并将其存放到 btr_search_sys->hash_table 的数组中,其中存放后的结构可以参考图 AHI memory structure;  

for (i = 0; i < n_cached; i++) {

		ha_insert_for_fold(table, folds[i], block, recs[i]);
	}

五 使用AHI


       AHI的目的是根据用户提供的查询条件加速定位到叶子节点,一般如果有固定的查询pattern,都可以通过AHI受益,尤其是Btree高度比较大的时候。
入口函数:btr_cur_search_to_nth_level  源码在:  innobase/btr/btr0cur.cc

/* Use of AHI is disabled for intrinsic table as these tables re-use
	the index-id and AHI validation is based on index-id. */
	if (rw_lock_get_writer(btr_get_search_latch(index))
		== RW_LOCK_NOT_LOCKED
	    && latch_mode <= BTR_MODIFY_LEAF
	    && info->last_hash_succ
	    && !index->disable_ahi
	    && !estimate
# ifdef PAGE_CUR_LE_OR_EXTENDS
	    && mode != PAGE_CUR_LE_OR_EXTENDS
# endif /* PAGE_CUR_LE_OR_EXTENDS */
	    && !dict_index_is_spatial(index)
	    /* If !has_search_latch, we do a dirty read of
	    btr_search_enabled below, and btr_search_guess_on_hash()
	    will have to check it again. */
	    && UNIV_LIKELY(btr_search_enabled)
	    && !modify_external
	    && btr_search_guess_on_hash(index, info, tuple, mode,
					latch_mode, cursor,
					has_search_latch, mtr)) {

  从代码段可以看出,需要满足如下条件才能够使用AHI:

  • 没有加btr_search_latch写锁。如果加了写锁,可能操作时间比较耗时,走AHI检索记录就得不偿失了;
  • latch_mode <= BTR_MODIFY_LEAF,表明本次只是一次不变更BTREE结构的DML或查询(包括等值、RANGE等查询)操作;
  • btr_search_info::last_hash_succ为true表示最近一次使用AHI成功(或可能成功)了;
  • 打开AHI开关;
  • 查询优化阶段的估值操作,例如计算range范围等,典型的堆栈包括:handler::multi_range_read_info_const –> ha_innobase::records_in_range –> btr_estimate_n_rows_in_range –> btr_cur_search_to_nth_level;
  • 不是spatial索引;
  • 调用者无需分配外部存储页(BTR_MODIFY_EXTERNAL,主要用于辅助写入大的blob数据,参考struct btr_blob_log_check_t)。
  • 当满足上述条件时,进入函数btr_search_guess_on_hash,根据当前的查询tuple对象计算fold,并查询AHI;只有当前检索使用的tuple列的个数大于等于构建AHI的列的个数时,才能够使用AHI索引。

btr_search_guess_on_hash 源码在innobase/btr/btr0sea.cc

  • 首先用户提供的前缀索引查询条件必须大于等于构建AHI时的前缀索引列数,这里存在一种可能性:索引上的search_info的n_fields 和block上构建AHI时的cur_n_fields值已经不相同了,但是我们并不知道本次查询到底落在哪个block上,这里一致以search_info上的n_fields为准来计算fold,去查询AHI;
  • 在检索AHI时需要加&btr_search_latch的S锁;
  • 如果本次无法命中AHI,就会将btr_search_info::last_hash_succ设置为false,这意味着随后的查询都不会去使用AHI了,只能等待下一路查询信息分析后才可能再次启动(btr_search_failure);
  • 对于从ahi中获得的记录指针,还需要根据当前的查询模式检查是否是正确的记录位置(btr_search_check_guess)。

如果本次查询使用了AHI,但查询失败了(cursor->flag == BTR_CUR_HASH_FAIL),并且当前block构建AHI索引的curr_n_fields等字段和btr_search_info上的相符合,则根据当前cursor定位到的记录插入AHI。参考函数:btr_search_update_hash_ref

从上述分析可见,AHI如其名,完全是自适应的,如果检索模式不固定,很容易就出现无法用上AHI或者AHI失效的情况。

六 shortcut查询模式

row_search_mvcc函数中,首先会去判断在满足一定条件时,使用shortcut模式,利用AHI索引来进行检索。

下面是我一开始文章的查询过程的第2步。

只有满足严苛的条件时(

1) 当前索引是 cluster index;
2) 当前查询是 unique search;
3) 当前查询不包含 blob 类型的大字段;
4) 记录长度不能大于 page_size/8;
5) 不是使用 memcache 接口协议的查询;
6) 事物开启且隔离级别大于 READ UNCOMMITTED;
7) 简单 select 查询而非在 function & procedure;,具体的参阅代码),才能使用shortcut:

在满足以上条件后才能使用 AHI 的 shortcut 查询方式定位叶子结点,5.7 中满足条件后的操作可以简单的描述为:
rw_lock_s_lock(btr_get_search_latch(index));
...
row_sel_try_search_shortcut_for_mysql()
...
rw_lock_s_lock(btr_get_search_latch(index));

七 AHI监控项

 MySQL 5.7 中有两个 AHI 相关的参数,分别为:innodb_adaptive_hash_index, innodb_adaptive_hash_index_parts,其中 innodb_adaptive_hash_index 为动态调整的参数,用以控制是否打开 AHI 功能;innodb_adaptive_hash_index_parts 是只读参数,在实例运行期间是不能修改,用于调整 AHI 分区的个数(5.7.8 引入),减少锁冲突,详细介绍可以参考官方说明:innodb_adaptive_hash_index, innodb_adaptive_hash_index

 监控项

 select status, name, subsystem,count, max_count, min_count, avg_count, time_enabled, time_disabled from INNODB_METRICS where subsystem like '%adaptive_hash%';

*************************************

   花了几天时间,仍然看的不是很明白。困惑的是row_search_mvcc 与 btr_cur_search_to_nth_level  ,感觉没有关联起来。

参考:

http://mysql.taobao.org/monthly/2015/09/01/

https://cloud.tencent.com/developer/article/1004516

https://dev.mysql.com/doc/refman/5.7/en/innodb-adaptive-hash.html

https://blog.csdn.net/along0314110/article/details/53982319

猜你喜欢

转载自blog.csdn.net/bohu83/article/details/82107598