我们来深入探讨 MySQL 中关键的数据结构容器(Containers),重点解析文档中提到的 DYNAMIC_ARRAY、List、I_P_List 和 LF_HASH。这些容器是 MySQL 内部管理和组织数据的核心基础设施,针对不同的使用场景(性能、线程安全、内存管理)进行了优化。
核心目标:为什么需要这些容器?
MySQL 需要高效、安全地管理各种类型的数据:
- 数据多样性: 连接列表、表缓存、用户权限信息、查询执行计划中的表列表、打开文件列表、锁信息、存储引擎内部结构(如 InnoDB 的缓冲池页列表、事务列表)等等。
- 操作需求: 需要支持插入、删除、查找、遍历、排序、动态扩容等操作。
- 性能要求: 对某些高频操作(如查找连接、缓存查找)要求 O(1) 或接近 O(1) 的时间复杂度;对内存使用要求高效。
- 线程安全: 在多线程环境中,容器需要提供安全的访问方式(加锁或无锁)。
- 内存管理: 需要与 MySQL 的内存管理机制(如
mysys分配器)集成。
解决方案:多样化的容器库
MySQL 在 include/ 和 sql/ 目录下实现了多种容器,各自有其适用场景:
-
DYNAMIC_ARRAY(动态数组)- 功能: 提供可动态扩容的数组(类似 C++ 的
std::vector或 Java 的ArrayList)。 - 特点:
- 连续内存: 元素存储在连续的内存块中,支持快速的随机访问(通过索引
[])。 - 动态扩容: 当数组空间不足时,自动申请更大的内存块(通常是当前大小的倍数),拷贝原有数据,并释放旧内存。
- 类型支持: 通过
void *数组实现泛型,可以存储任意类型的数据(基本类型、结构体、指针等)。使用者负责类型转换。 - 相对简单: 实现比链表和哈希表简单。
- 非线程安全: 默认情况下不是线程安全的,需外部同步(如
native_mutex_t)保护。
- 连续内存: 元素存储在连续的内存块中,支持快速的随机访问(通过索引
- 核心结构 (简化 - 通常在
my_sys.h或类似位置):typedef struct st_dynamic_array { uchar *buffer; // 指向实际存储数据的连续内存块 uint elements; // 当前存储的元素个数 uint max_element; // 当前分配内存能容纳的最大元素个数 uint alloc_increment; // 每次扩容时增加的元素个数 uint size_of_element; // 单个元素的大小(字节) } DYNAMIC_ARRAY; - 核心操作 (示例):
my_init_dynamic_array(DYNAMIC_ARRAY *array, uint element_size, uint init_alloc, uint alloc_increment): 初始化数组,指定元素大小、初始容量、扩容增量。insert_dynamic(DYNAMIC_ARRAY *array, uchar *element): 在数组末尾插入一个元素(可能触发扩容)。get_dynamic(DYNAMIC_ARRAY *array, uint idx): 返回指向索引idx处元素的指针 (uchar *)。使用者需强制转换为实际类型。delete_dynamic(DYNAMIC_ARRAY *array): 删除数组,释放内存。pop_dynamic(DYNAMIC_ARRAY *array): 删除并返回最后一个元素(如果数组非空)。sort_dynamic(DYNAMIC_ARRAY *array, int (*cmp)(const void *, const void *)): 使用指定的比较函数对数组排序。
- 优势:
- 随机访问快:
O(1)时间复杂度。 - 尾部插入/删除快:
O(1)平均时间复杂度(不考虑扩容)。扩容是O(n),但均摊后通常可接受。 - 内存局部性好: 连续存储利于 CPU 缓存命中。
- 随机访问快:
- 劣势:
- 中间插入/删除慢:
O(n)时间复杂度,需要移动大量元素。 - 扩容开销: 扩容时涉及内存分配、拷贝和释放,可能带来瞬时开销。
- 中间插入/删除慢:
- 典型应用场景:
- 存储相对固定或尾部操作居多的数据集合。
- 需要快速随机访问的场景。
- 例如:某些查询执行阶段的结果集缓冲区、解析 SQL 时存储 Token 列表、一些配置参数列表。
- 功能: 提供可动态扩容的数组(类似 C++ 的
-
List(侵入式双向链表)- 功能: 提供一种侵入式(Intrusive) 的双向链表实现。这是一种非常经典的设计模式(类似 Linux 内核的
list_head)。 - 特点:
- 侵入式: 链表节点 (
LIST) 直接嵌入在用户数据结构体中。这意味着:- 链表操作无需额外的内存分配来存储节点。
- 用户数据结构体必须包含
LIST成员。 - 通过
LIST成员获取包含它的用户结构体需要使用offsetof/container_of宏(在 MySQL 中通常是list_element()宏)。
- 双向: 每个节点有
prev和next指针,支持双向遍历。 - 高效插入/删除: 已知节点位置时的插入和删除操作是
O(1)。 - 非线程安全: 默认非线程安全。
- 侵入式: 链表节点 (
- 核心结构 (通常在
sql/sql_list.h):typedef struct st_list { struct st_list *prev, *next; } LIST; typedef struct st_list_iterator { LIST *current; } LIST_ITERATOR; - 核心操作 (示例):
list_init(LIST *root): 初始化一个空链表(root->next = root->prev = root)。list_add(LIST *root, LIST *element): 将element添加到链表头部 (root->next)。list_add_tail(LIST *root, LIST *element): 将element添加到链表尾部 (root->prev)。list_remove(LIST *element): 将element从链表中移除(需确保element在链表中)。list_empty(LIST *root): 判断链表是否为空。list_first(LIST *root): 返回链表第一个元素(非root节点)。list_last(LIST *root): 返回链表最后一个元素(非root节点)。list_next(LIST *root, LIST *element): 返回element的下一个元素。list_prev(LIST *root, LIST *element): 返回element的前一个元素。list_iterate(LIST *root, LIST_ITERATOR *iter): 初始化迭代器。list_element(LIST *ptr, type, member): 关键宏! 通过指向LIST成员 (member) 的指针ptr,获取包含该成员的用户结构体 (type) 的指针。例如:TABLE_LIST *table = list_element(ptr, TABLE_LIST, next_in_list)。
- 优势:
- 空间效率高: 无额外节点内存分配开销。
- 插入/删除快:
O(1)。 - 灵活: 一个结构体可以同时属于多个链表(嵌入多个
LIST成员)。 - 内存碎片少: 节点嵌入在用户对象中,用户对象通常由特定池分配器管理。
- 劣势:
- 查找慢: 需要遍历链表
O(n)。 - 内存局部性差: 节点分散在内存中,不利于缓存。
- 侵入式设计: 需要修改用户数据结构体(加入
LIST成员)。
- 查找慢: 需要遍历链表
- 典型应用场景:
TABLE_LIST(查询中使用的表列表)。Item(表达式树节点)的子项列表。ORDER(ORDER BY子句列表)。KEY_MULTI_RANGE(范围扫描列表)。- 存储引擎内部的链表结构(如 InnoDB 的事务列表的一部分)。
- 很多需要
O(1)插入/删除且查找不是主要瓶颈的列表。
- 功能: 提供一种侵入式(Intrusive) 的双向链表实现。这是一种非常经典的设计模式(类似 Linux 内核的
-
I_P_List(侵入式保护链表)- 功能: 在基本
List的基础上,增加了线程安全保护(Protected) 的侵入式双向链表。 - 特点:
- 继承自
List: 核心链表操作基于侵入式双向链表。 - 线程安全保护: 内部集成了一把互斥锁(通常是
native_mutex_t或包装)。任何对链表的操作(插入、删除、查找、遍历)都需要先获取这把锁。 - 明确的锁策略: 通过模板参数(或宏定义)指定使用的锁类型,提供一定的灵活性。
- 迭代器集成锁: 通常提供安全的迭代器,在迭代器生命周期内持有锁(或提供显式的
lock/unlock方法)。
- 继承自
- 目的: 简化在多线程环境中使用链表的安全性。使用者无需自己管理锁,
I_P_List负责在操作期间持有锁。 - 核心操作 (概念类似
List, 但操作自动加锁):push_front(element),push_back(element): 加锁后在头/尾部插入元素。pop_front(),pop_back(): 加锁后删除头/尾部元素。remove(element): 加锁后移除指定元素。is_empty(): 加锁后判断是否为空。front(),back(): 加锁后访问头/尾部元素。iterator begin(),iterator end(): 获取迭代器(迭代器内部可能持有锁或要求在锁保护下使用)。
- 优势:
- 线程安全: 内置锁机制,防止并发访问导致的数据损坏。
- 简化开发: 封装了锁的获取和释放,降低开发者出错概率。
- 基于高效基础: 底层是高效的侵入式链表。
- 劣势:
- 锁开销: 每次操作都有加锁/解锁的开销,可能成为性能瓶颈(高竞争时)。
- 迭代阻塞: 遍历整个链表时长时间持有锁,会阻塞其他线程的访问。
- 典型应用场景:
- 需要线程安全访问的全局列表。
- 修改频率相对较低,或者竞争不激烈的共享链表。
- 例如:全局连接列表(
THD列表)、插件列表、某些后台任务队列。
- 功能: 在基本
-
LF_HASH(无锁哈希表)- 功能: 提供一种无锁(Lock-Free) 的哈希表实现,旨在实现极高的并发读/写性能。
- 特点:
- 无锁(Lock-Free): 核心算法不使用传统的互斥锁(mutex)或读写锁(rwlock)来实现并发控制。 这避免了线程阻塞导致的上下文切换开销,在高并发场景下性能显著优于加锁的方案。
- 原子操作依赖: 实现高度依赖 CPU 提供的原子操作(Atomic Operations)(如
compare_and_swap- CAS,fetch_and_add)和内存屏障(Memory Barriers)。 - 哈希桶: 基础结构仍然是数组 + 链表(或其他冲突解决法如开放寻址,但 MySQL 的
LF_HASH通常是拉链法)。 - 动态扩展: 支持动态扩容(rehash),但过程通常比较复杂且需要原子性保证。
- 版本号/状态标记: 常用技巧是在节点或指针中使用版本号或状态标记来解决 ABA 问题(一个值从 A 变 B 又变回 A,CAS 误以为没变)。
- 内存管理挑战: 无锁数据结构最大的难点在于安全的内存回收(当一个节点被删除时,如何确定没有线程还在访问它?)。MySQL 的
LF_HASH通常采用 Hazard Pointer(危险指针) 或类似 Quiescent State Based Reclamation (QSBR) 的机制(在 MySQL 上下文中常称为 RCU - Read-Copy-Update 思想)。这需要与 MySQL 的线程调度/等待点结合。 - 读操作通常无等待: 理想情况下,读操作完全不需要等待或与其他写操作冲突。
- 写操作可能重试: 写操作(插入、删除、更新)在遇到冲突时(CAS 失败),通常会重试(retry-loop)。
- 核心思想 (简化):
- 插入:
- 创建新节点。
- 根据 key 找到桶。
- 使用 CAS 原子操作将新节点的
next指向桶的当前头节点。 - 使用 CAS 原子操作将桶的头指针更新为新节点。如果失败(其他线程已修改),则重试步骤 3-4 或从步骤 2 开始。
- 删除 (逻辑删除):
- 根据 key 找到节点。
- 标记(Marking): 使用原子操作将节点标记为“已删除”(如设置指针中的特定位)。
- 物理断开: 使用 CAS 原子操作将节点的前驱节点的
next指针指向被删节点的后继节点。如果前驱节点也被删了,可能需要回溯。 - 物理回收: 物理内存的回收会延迟到安全点(Safe Point),确保没有线程持有指向该节点的危险指针。
- 查找:
- 根据 key 找到桶。
- 遍历桶链表。
- 遇到标记为删除的节点可以跳过(取决于实现阶段)。
- 找到匹配的 key 则返回数据。
- 插入:
- 优势:
- 超高并发性能: 尤其在高读比例或中等写比例的场景下,性能远胜于基于锁的哈希表。读操作几乎无冲突。
- 无优先级反转/死锁: 因为无锁,不会发生传统锁导致的问题。
- 劣势:
- 实现极其复杂: 正确实现无锁算法非常困难,调试困难。
- ABA 问题: 需要精心设计(版本号)来解决。
- 内存回收复杂: Hazard Pointer 或 RCU/QSBR 机制增加了复杂性和内存开销(需要存储危险指针列表或识别线程静止期)。
- CPU 消耗: 写操作在高竞争下可能导致大量 CAS 重试,消耗 CPU。
- 功能限制: 可能不支持某些复杂的操作(如按条件批量删除),或操作代价较高。
- 典型应用场景:
- MySQL 性能模式(
performance_schema)中的许多表(如events_waits_current,events_statements_current)。这是LF_HASH最著名的应用。 - 需要极高并发访问的全局缓存(如果键的设计允许高效哈希)。
- 连接查询缓存(如果存在)。
- InnoDB 自适应哈希索引(AHI)虽然叫哈希索引,但它内部使用的同步机制与
LF_HASH的核心思想(无锁、CAS)有相似之处,但实现细节不同且更复杂。
- MySQL 性能模式(
总结与对比:如何选择合适的容器?
| 特性 | DYNAMIC_ARRAY | List (侵入式) | I_P_List (保护链表) | LF_HASH (无锁哈希) |
|---|---|---|---|---|
| 核心结构 | 动态数组 | 侵入式双向链表 | 侵入式双向链表 + 锁 | 无锁哈希桶 (拉链法) |
| 随机访问 | O(1) (索引) | O(n) (遍历) | O(n) (遍历) | O(1) avg (哈希查找) |
| 插入/删除 | 尾部 O(1) avg, 中间 O(n) | O(1) (已知位置) | O(1) (需锁) | O(1) avg (CAS 可能重试) |
| 查找 | O(n) (线性搜索) | O(n) (线性搜索) | O(n) (线性搜索, 需锁) | O(1) avg (哈希查找) |
| 线程安全 | 否 (需外部锁) | 否 (需外部锁) | 是 (内置锁) | 是 (Lock-Free 算法) |
| 内存效率 | 中 (可能有未用空间) | 高 (无额外节点) | 高 (无额外节点, 锁额外) | 中 (桶+节点, Hazard Pointers 开销) |
| 内存局部性 | 高 (连续) | 低 (节点分散) | 低 (节点分散) | 中 (桶连续, 链表节点分散) |
| 典型场景 | 结果集缓冲, 配置列表 | 查询表列表, 表达式树 | 全局线程安全列表 (THD, 插件) | 超高并发查找 (performance schema) |
结论:
DYNAMIC_ARRAY、List、I_P_List 和 LF_HASH 代表了 MySQL 内部针对不同需求(随机访问 vs 顺序访问、查找速度 vs 插入速度、内存效率 vs 并发性能、线程安全要求)精心设计和实现的核心容器库。
- 需要高效随机访问和尾部操作?考虑
DYNAMIC_ARRAY。 - 需要高效插入/删除且内存敏感?考虑侵入式
List。 - 需要线程安全的链表?考虑
I_P_List(注意锁开销)。 - 需要超高性能、高并发的键值查找?考虑无锁的
LF_HASH(接受其复杂性和限制)。
理解这些容器的设计原理、优缺点和适用场景,是深入理解 MySQL 内核工作机制(如查询执行、连接管理、性能监控、存储引擎内部)的基础。它们共同构成了 MySQL 高效、稳定处理海量数据和并发请求的数据管理基石。