一句话结论(30s)
AHI 的本质是 InnoDB 在 BufferPool 内自动构建的一张内存哈希表,把频繁访问的索引键映射到「叶子页地址」而非行,从而把等值查询的 O(log n) 层级遍历降为一次哈希 + 页内二分。关键设计是「自适应」——访问计数达阈值才建、长期不用或页分裂时自动删,且分 8 个分区锁分离;核心权衡是它只加速等值查询、范围/LIKE 不受益,且高并发写入时每次页修改都要维护哈希键,维护开销可能反超收益,故写入密集场景建议关闭。
核心原理(2min)
B+Tree 即使全命中 BufferPool 仍需 2-4 层页内二分定位,AHI 用取模散列(ut_fold_binary 折叠)把键映射到叶子页地址,跳过层级遍历后仅在页内二分。哈希键 = index_id + key_bytes,因此同一键值在不同索引上是不同哈希键。AHI 分 8 个分区(innodb_adaptive_hash_index_parts)实现锁分离并行访问。构建触发条件是某索引页被等值查找计数达阈值,范围扫描与 LIKE 不计数;当键长期未用或页大量修改(分裂/合并)时自动删除。代价是占用 BufferPool 内存、每次写页都需检查并可能删键;用 hash searches/s 与 non-hash searches/s 计算命中率,命中率持续很低或写入密集时应关闭。
底层深入(5-10min)
一、AHI 是什么?为什么需要它?
InnoDB 的数据访问路径:根页 → B+Tree 内部节点 → 叶子节点。即使在 BufferPool 中全部命中,定位一行数据也需要 O(log n) 次页内二分查找(log 以页的扇出因子为底,通常在 2-4 层)。
对于高频的等值查询(如 WHERE pk = 100 或 WHERE unique_key = 'xxx'),每次都要走 B+Tree 路径是一种浪费。自适应哈希索引(Adaptive Hash Index, AHI) 就是为此设计的:InnoDB 自动在 BufferPool 中构建一张哈希表,将频繁访问的索引键直接映射到 BufferPool 中的 页地址。
关键理解:AHI 不是传统意义上的持久化索引。它不存储在磁盘上,不占用独立磁盘空间,完全是内存结构。它随 BufferPool 一起加载和清除,崩溃后重建。
💭 想一想:既然 B+Tree 已经能 O(log n) 定位了,为什么还要再造一张哈希表?——因为”在内存里走 2-4 层 B+Tree、每层一次页内二分”对高频等值查询仍是浪费;哈希能把”多步层级遍历”压成”一次取模 + 一次页内二分”。它赌的是”很多查询反复命中同样的键”,命中时省掉的那几层遍历累积起来就很可观。
1.1 哈希键 → 页地址,不等于哈希键 → 行
这是一个常被误解的点。AHI 不直接将索引值映射到行数据,而是映射到 包含该键的叶子页。原因是一个叶子页包含多条相邻记录(B+Tree 的有序扇出),映射到页后仍需在页内二分查找具体记录,但跳过了 B+Tree 的层级遍历(一般跳过 2-3 层)。
💭 想一想:为什么宁可”映射到页再页内二分”,也不一步到位映射到”行”?——因为一个叶子页里有几十上百条相邻记录,若每个键都单独建一条”键→行”映射,哈希表会膨胀到和索引本身一样大、维护成本爆炸;映射到页既能大幅减少表项数量,又能把最贵的”层级遍历”省掉,剩下的页内二分已经很快。这是一次”表大小”与”省多少步”之间的折衷。
二、AHI 的哈希结构
2.1 哈希函数
InnoDB 使用的哈希函数是 取模散列:
// InnoDB 源码中的哈希计算(简化)
hash_value = ut_fold_binary(key, key_len) % hash_table_size;
// ut_fold_binary:将字节序列折叠为整数,类似 CRC32-lite
不是通用安全的 SHA/CRC,而是速度优先的折叠函数——兼顾分布均匀性和计算速度。
2.2 哈希表组织
AHI 使用 分区的锁分离设计:将哈希表划分为多个分区(默认 8 个,由 innodb_adaptive_hash_index_parts 控制),每个分区有独立的读写锁。这样多个查询可以并行访问 AHI,减少锁竞争。
AHI 哈希表(内存):
Partition 0: Bucket 0 → (key=0x01, page=0x200) → (key=0x03, page=0x201) → ...
Bucket 1 → ...
Partition 1: Bucket 0 → ...
...
Partition 7: Bucket 0 → ...
2.3 键的构成
// AHI 的哈希键(源码结构简化)
struct ha_key {
byte[] index_id; // 索引 ID(表级别唯一)
byte[] key_bytes; // 索引键的二进制值
};
这意味着同一个主键值在不同的索引上产生不同的哈希键——因为 index_id 不同。
三、自动创建与触发条件
AHI 的”自适应”体现在它不手动创建,而是 InnoDB 根据访问模式自动决策。
3.1 创建触发条件
InnoDB 对每个索引页维护一个访问计数器。当一个索引页被 等值查找 的次数达到阈值时,该页上的键值被添加到 AHI 中:
1. B+Tree 等值查找命中某叶子页
2. 该页的访问计数 +1
3. 当计数 >= innodb_adaptive_hash_index_parts * 某个内部阈值
4. 将该页中频繁访问的键写入 AHI 哈希表
注意是 等值查找 才会被计数。范围扫描(WHERE id BETWEEN 100 AND 200)、LIKE 前缀查询不会触发 AHI 构建——因为它们无法用哈希表加速(哈希只能做精确匹配)。
💭 想一想:为什么范围/LIKE 天然与哈希无缘?——因为哈希的本质是”精确键 → 地址”的点查找;范围查询要的是”一个区间内所有连续记录”,而哈希把有序关系打散了,区间里的记录散落在不同桶里,没法顺序遍历。所以 AHI 只对
=、IN、联合索引完整等值这类”点查”有效,这决定了它的适用边界。
3.2 自动删除条件
AHI 是动态的——如果某个键长期未被使用,当哈希表空间不足时会被踢出。此外,当索引页发生大量修改(分裂、合并)时,相关键也会被自动删除。
3.3 哪些查询受益?
| 查询类型 | AHI 是否生效 | 原因 |
|---|---|---|
WHERE pk = 1 | ✅ 完全受益 | 等值查询,主键哈希直接定位页 |
WHERE uk = 'X' | ✅ 完全受益 | 等值查询,唯一索引哈希定位页 |
WHERE col IN (1,2,3) | ✅ 部分受益 | 每个值走一次哈希 |
WHERE col BETWEEN 1 AND 100 | ❌ 不受益 | 范围查询,AHI 只支持等值 |
INSERT/UPDATE/DELETE | ❌ 不受益 | 写入路径不走 AHI(但可能触发删除) |
WHERE col LIKE 'abc%' | ❌ 不受益 | 前缀匹配不是等值 |
联合索引 WHERE a=1 AND b=2 | ✅ 受益 | 等值查询,键 = (a,b) 的完整值 |
四、监控与查看
4.1 SHOW ENGINE INNODB STATUS
SHOW ENGINE INNODB STATUS\G
-- 查看 INSERT BUFFER AND ADAPTIVE HASH INDEX 段
输出示例:
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 0, seg size 2, 0 merges
Hash table size 276671, node heap has 1 buffer(s)
Hash table size 276671, node heap has 2 buffer(s)
0.00 hash searches/s, 0.00 non-hash searches/s
关键指标解读:
| 指标 | 含义 | 健康值 |
|---|---|---|
hash searches/s | 走 AHI 的查询次数 | 越高越好 |
non-hash searches/s | 没走 AHI 的查询次数 | 越低越好 |
Hash table size | AHI 桶数量 | 自适应增长 |
node heap has N buffer(s) | AHI 节点占用的 BufferPool 页数 | 应保持在合理范围 |
4.2 AHI 命中率计算
-- 两次采样之间的 AHI 命中率
-- 第一次采样
SHOW ENGINE INNODB STATUS\G
-- 记录 hash_searches_1, non_hash_searches_1
-- 等待 10 秒
SHOW ENGINE INNODB STATUS\G
-- 记录 hash_searches_2, non_hash_searches_2
-- 命中率 = (hash_searches_2 - hash_searches_1)
-- / ((hash_searches_2 - hash_searches_1) + (non_hash_searches_2 - non_hash_searches_1))
4.3 动态开关
-- 关闭 AHI(临时)
SET GLOBAL innodb_adaptive_hash_index = OFF;
-- 开启 AHI
SET GLOBAL innodb_adaptive_hash_index = ON;
-- 调整分区数(重启生效)
-- innodb_adaptive_hash_index_parts = 8 # 默认值
五、AHI 的代价与限制
5.1 内存占用
AHI 占用的内存来自 BufferPool,与数据页、索引页竞争 [1]。在高连接数下,AHI 可能占用数百 MB 甚至数 GB 内存。当 BufferPool 紧张时,InnoDB 会优先淘汰 AHI 节点。
-- 查看 Buffer Pool 总大小
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
-- 查看 Buffer Pool 中 AHI 的占用(通过 information_schema)
SELECT * FROM information_schema.innodb_buffer_page
WHERE page_type = 'ADAPTIVE_HASH_INDEX';
5.2 写入性能下降
每次 B+Tree 的页修改(INSERT/UPDATE/DELETE 导致页分裂、合并或记录变更),都需要检查并可能删除 AHI 中对应的键。在高并发写入场景下,AHI 的维护开销可能超过其加速收益。
典型反模式:OLTP 写入密集型业务 + 大量等值查询。AHI 降低了单条查询的延迟,但每次写入都伴随着 AHI 的维护操作。需要根据 hash searches/s 命中率判断是否值得。
💭 想一想:AHI 明明是”加速器”,为什么高并发写场景反而建议关掉?——因为加速只发生在读的那一侧,而每次写(页分裂、合并、记录变更)都要反过来维护哈希键、甚至删键;写入一密集,维护开销就压过了省下的那几层遍历。这是”读收益”与”写成本”的天平,命中率低或写密集时天平就倒向”关掉更划算”。
5.3 何时关闭
- 系统 90% 以上是范围查询或 LIKE 查询
- hash searches/s 持续 < 10% 的查询总量
- 高并发写入场景下 BufferPool 争抢严重
- 内存资源极度紧张(BufferPool < 512MB)
章末提问
追问 1:AHI 为什么映射到”叶子页地址”而不是”行地址”?
回答思路:结论先行——为了控制哈希表规模:映射到页只省”层级遍历”,页内仍二分。因为一个叶子页包含几十上百条相邻记录,若每个键都建”键→行”映射,哈希表会膨胀到和索引一样大、维护成本爆炸;映射到页能让表项数量大幅减少,同时把最贵的 2-3 层 B+Tree 遍历省掉,剩下的页内二分已经足够快。
追问 2:AHI 的”自适应”体现在哪里?哪些查询受益、哪些不受益?
回答思路:结论先行——体现在”按访问计数自动建、长期不用或页分裂自动删、分 8 分区锁分离”;只有等值查询受益,范围/LIKE/写入不受益。因为哈希只能做”精确键→地址”的点查,=、IN、联合索引完整等值能受益,BETWEEN、LIKE 'abc%' 需要顺序遍历、与哈希的有序性相悖。建触发靠”索引页等值查找计数达阈值”,删靠”长期不用或页大量修改”。
追问 3:什么场景应该关闭 AHI?为什么写密集时反而有害?
回答思路:结论先行——范围/LIKE 为主、命中率持续很低、高并发写、BufferPool 紧张(<512MB)时关闭。因为 AHI 的收益只在”读”这一侧省层级遍历,而每次写(页分裂/合并/记录变更)都要维护或删除哈希键,写一密集维护成本就反超读收益;命中率靠 hash searches/s 与 non-hash searches/s 的比值判断,持续很低就说明白建了,不如关掉省内存、省写维护开销。