MySQL 中关键的数据结构容器(Containers)

52 阅读12分钟

我们来深入探讨 MySQL 中关键的数据结构容器(Containers),重点解析文档中提到的 DYNAMIC_ARRAYListI_P_ListLF_HASH。这些容器是 MySQL 内部管理和组织数据的核心基础设施,针对不同的使用场景(性能、线程安全、内存管理)进行了优化。

核心目标:为什么需要这些容器?

MySQL 需要高效、安全地管理各种类型的数据:

  1. 数据多样性: 连接列表、表缓存、用户权限信息、查询执行计划中的表列表、打开文件列表、锁信息、存储引擎内部结构(如 InnoDB 的缓冲池页列表、事务列表)等等。
  2. 操作需求: 需要支持插入、删除、查找、遍历、排序、动态扩容等操作。
  3. 性能要求: 对某些高频操作(如查找连接、缓存查找)要求 O(1) 或接近 O(1) 的时间复杂度;对内存使用要求高效。
  4. 线程安全: 在多线程环境中,容器需要提供安全的访问方式(加锁或无锁)。
  5. 内存管理: 需要与 MySQL 的内存管理机制(如 mysys 分配器)集成。

解决方案:多样化的容器库

MySQL 在 include/sql/ 目录下实现了多种容器,各自有其适用场景:

  1. 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 列表、一些配置参数列表。
  2. List (侵入式双向链表)

    • 功能: 提供一种侵入式(Intrusive) 的双向链表实现。这是一种非常经典的设计模式(类似 Linux 内核的 list_head)。
    • 特点:
      • 侵入式: 链表节点 (LIST) 直接嵌入在用户数据结构体中。这意味着:
        • 链表操作无需额外的内存分配来存储节点。
        • 用户数据结构体必须包含 LIST 成员。
        • 通过 LIST 成员获取包含它的用户结构体需要使用 offsetof/container_of 宏(在 MySQL 中通常是 list_element() 宏)。
      • 双向: 每个节点有 prevnext 指针,支持双向遍历。
      • 高效插入/删除: 已知节点位置时的插入和删除操作是 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(表达式树节点)的子项列表。
      • ORDERORDER BY 子句列表)。
      • KEY_MULTI_RANGE(范围扫描列表)。
      • 存储引擎内部的链表结构(如 InnoDB 的事务列表的一部分)。
      • 很多需要 O(1) 插入/删除且查找不是主要瓶颈的列表。
  3. 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 列表)、插件列表、某些后台任务队列。
  4. 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)。
    • 核心思想 (简化):
      • 插入:
        1. 创建新节点。
        2. 根据 key 找到桶。
        3. 使用 CAS 原子操作将新节点的 next 指向桶的当前头节点。
        4. 使用 CAS 原子操作将桶的头指针更新为新节点。如果失败(其他线程已修改),则重试步骤 3-4 或从步骤 2 开始。
      • 删除 (逻辑删除):
        1. 根据 key 找到节点。
        2. 标记(Marking): 使用原子操作将节点标记为“已删除”(如设置指针中的特定位)。
        3. 物理断开: 使用 CAS 原子操作将节点的前驱节点的 next 指针指向被删节点的后继节点。如果前驱节点也被删了,可能需要回溯。
        4. 物理回收: 物理内存的回收会延迟到安全点(Safe Point),确保没有线程持有指向该节点的危险指针。
      • 查找:
        1. 根据 key 找到桶。
        2. 遍历桶链表。
        3. 遇到标记为删除的节点可以跳过(取决于实现阶段)。
        4. 找到匹配的 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)有相似之处,但实现细节不同且更复杂。

总结与对比:如何选择合适的容器?

特性DYNAMIC_ARRAYList (侵入式)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_ARRAYListI_P_ListLF_HASH 代表了 MySQL 内部针对不同需求(随机访问 vs 顺序访问、查找速度 vs 插入速度、内存效率 vs 并发性能、线程安全要求)精心设计和实现的核心容器库。

  • 需要高效随机访问和尾部操作?考虑 DYNAMIC_ARRAY
  • 需要高效插入/删除且内存敏感?考虑侵入式 List
  • 需要线程安全的链表?考虑 I_P_List(注意锁开销)。
  • 需要超高性能、高并发的键值查找?考虑无锁的 LF_HASH(接受其复杂性和限制)。

理解这些容器的设计原理、优缺点和适用场景,是深入理解 MySQL 内核工作机制(如查询执行、连接管理、性能监控、存储引擎内部)的基础。它们共同构成了 MySQL 高效、稳定处理海量数据和并发请求的数据管理基石。