TDengine MemTable 深度解析 — 内存写入缓冲区的数据结构与生命周期

分类 :3.存储引擎 | 篇章:02 MemTable

适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-05-25

MemTable 是 VNode 中数据从写入到落盘之间的内存暂存区。它使用跳表(Skip List)按时间有序组织数据,既保证高效的并发写入,又支持快速的范围查询。本文解析 MemTable 的内部数据结构、生命周期管理和内存回收机制。

核心概念速查表

概念 说明
MemTable 当前活跃的内存写入缓冲区
Immutable MemTable (IMem) 已冻结、正在刷盘的旧 MemTable
Skip List 跳表,MemTable 中数据的核心有序结构
Buffer Pool 内存池,MemTable 的内存来源(3 段轮转)
STbData 单张子表在 MemTable 中的数据容器
Commit 触发 MemTable 写满时触发刷盘流程

详细解析

1. MemTable 的整体结构

复制代码
MemTable 内部组织:

  MemTable
  │
  ├── Hash Table: uid → STbData
  │   (快速定位某张子表的数据)
  │
  ├── RB-Tree: 按 uid 排序的子表集合
  │   (顺序遍历所有子表,用于 Commit)
  │
  └── 统计信息:
        - nRows: 总行数
        - minVer / maxVer: 版本范围
        - minKey / maxKey: 时间戳范围

2. 子表数据容器(STbData)

每张子表在 MemTable 中有独立的数据容器:

复制代码
STbData 结构:

  STbData (uid = 子表 UID)
  │
  ├── suid: 所属超级表 UID
  ├── uid: 子表 UID
  │
  └── Skip List(5 层跳表)
        │
        ├── Level 4: ──○────────────────────○──────
        ├── Level 3: ──○──────○─────────────○──────
        ├── Level 2: ──○──○───○──○──────────○──────
        ├── Level 1: ──○──○───○──○──○───○───○──────
        └── Level 0: ──○──○──○──○──○──○──○──○──○──
                       ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓
                      行 行 行 行 行 行 行 行 行
                      (按时间戳有序)

3. 跳表的工作原理

跳表是 MemTable 的核心数据结构,提供 O(log N) 的插入和查找性能:

复制代码
跳表操作复杂度:

  插入一行数据:O(log N)
  按时间范围查询:O(log N + M)  (N=总行数,M=结果行数)
  查找特定时间戳:O(log N)
  
  层数:5 层(固定)
  晋升概率:每层约 1/4 的节点晋升到上层
  
  内存布局:
    节点 = 前向指针数组 + 数据行指针
    数据行 = 时间戳 + 各列值(紧凑编码)

为什么选择跳表而不是 B+树?

对比 跳表 B+树
并发写入 无锁/细粒度锁,高并发友好 需要树结构锁
实现复杂度 简单 复杂(分裂/合并)
内存分配 节点独立分配 页内批量分配
范围查询 O(log N + M) O(log N + M),相同
适合场景 追加写入为主 随机读写均衡

4. 数据写入 MemTable 的流程

复制代码
INSERT 写入 MemTable 流程:

  写入请求(一批行数据)
       │
       ▼
  ① 从 Buffer Pool 分配内存块
       │
       ▼
  ② 按子表 UID 查找 Hash Table:
     - 命中 → 获取现有 STbData
     - 未命中 → 创建新 STbData,插入 Hash Table 和 RB-Tree
       │
       ▼
  ③ 对每一行:
     a. 编码行数据(时间戳 + 列值 → 紧凑二进制格式)
     b. 插入跳表:
        - 比较时间戳找到插入位置
        - 随机决定层数(1~5 层)
        - 链接前向指针
     c. 如果时间戳已存在 → 覆盖更新(同表同时间戳只保留最新)
       │
       ▼
  ④ 更新统计信息:
     - nRows++
     - 更新 minKey / maxKey
     - 更新 minVer / maxVer

5. MemTable 生命周期

复制代码
MemTable 状态机:

  创建(Active)
    │
    │ ← 接收写入
    │
    │ 触发条件满足(Buffer 段写满)
    │
    ▼
  冻结(Immutable)
    │
    │ ← 不再接收写入,只供读取
    │ ← Commit 线程异步刷盘
    │
    │ 刷盘完成
    │
    ▼
  回收(Released)
    │
    │ 内存归还 Buffer Pool
    │
    ▼
  销毁

6. Buffer Pool 与 MemTable 的关系

复制代码
Buffer Pool 三段轮转与 MemTable:

  BUFFER = 256 MB(用户配置)
  
  ┌─────────────────────────────────────────────┐
  │              Buffer Pool                      │
  │                                               │
  │  段 A (85MB)    段 B (85MB)    段 C (85MB)   │
  │  [Active Mem]   [Imm Mem]     [Free]         │
  │                                               │
  │  状态轮转:                                    │
  │  A: 当前写入 → 写满后冻结                      │
  │  B: 正在刷盘 → 刷盘完成后变为 Free             │
  │  C: 空闲 → A 写满后接替为 Active               │
  └─────────────────────────────────────────────┘
  
  如果 C 还没来得及释放(B 刷盘慢):
    → 写入被阻塞(背压机制)
    → 日志警告:"Write is blocked due to buffer full"

7. Commit 触发条件

MemTable 刷盘由以下条件触发:

触发条件 说明
Buffer 段写满 当前段使用量达到 BUFFER/3
手动 FLUSH 用户执行 FLUSH DATABASE
VNode 关闭 正常关闭时刷盘所有未持久化数据
WAL 大小超限 WAL 累计超过上限时加速刷盘

8. 数据在 MemTable 中的编码

复制代码
行数据的内存编码格式:

  ┌──────────────────────────────────────┐
  │          一行数据(Row)               │
  ├──────────┬──────────┬────────────────┤
  │ 行头     │ NULL位图  │ 列值数组        │
  │ (版本号  │ (每列1bit │ (按定义顺序    │
  │  +标志)  │  标记NULL) │  紧凑排列)     │
  └──────────┴──────────┴────────────────┘
  
  定长列:直接存储值(INT=4字节, FLOAT=4字节...)
  变长列:长度前缀(2字节) + 实际数据
  
  整行大小 = 行头 + ⌈列数/8⌉ + Σ(各列大小)

9. MemTable 与查询的交互

查询需要同时读取 MemTable 和磁盘文件,通过多路归并保证结果完整:

复制代码
查询读取 MemTable:

  TableScan 算子
       │
       ├── 从 Active MemTable 读取
       │     └── 跳表范围查询 [startTs, endTs]
       │
       ├── 从 Immutable MemTable 读取(如果存在)
       │     └── 同上
       │
       └── 从磁盘文件读取
             └── .head → .data → 解压
       │
       ▼
  归并排序(按时间戳,取最新版本)
       │
       ▼
  输出有序结果

MVCC 语义:每行数据带有写入版本号(commitVer),查询使用 snapshot version 过滤------只看到查询开始前已提交的数据。

10. 乱序数据处理

时序数据可能乱序到达(网络延迟、设备补报等):

复制代码
乱序数据写入处理:

  MemTable 跳表中数据按时间有序排列
  
  Case 1: 新数据时间戳 > 当前最大时间戳(正常顺序)
    → 追加到跳表末尾 → O(1) 快速路径
    
  Case 2: 新数据时间戳 < 当前最大时间戳(乱序)
    → 二分查找插入位置 → O(log N)
    → 插入到跳表中间
    
  Case 3: 新数据时间戳 = 已有时间戳(重复/更新)
    → 覆盖已有行(保留最新写入)
    
  乱序对性能的影响:
  - 写入性能:轻微降低(从 O(1) 变为 O(log N))
  - 落盘合并:乱序数据可能需要写入旧的 FileSet
  - STT 设计正是为了高效处理乱序落盘场景

代码示例

监控 MemTable 状态

sql 复制代码
-- 查看 VNode 内存使用情况
SHOW VGROUPS;
-- 关注各 VNode 的内存占用

-- 手动触发刷盘
FLUSH DATABASE power;

-- 调整 Buffer 大小
ALTER DATABASE power BUFFER 512;

观察写入背压

bash 复制代码
# 如果写入持续被阻塞,taosd 日志中会出现:
# "vgId:X write is blocked since buffer is full"
# 解决方案:
# 1. 增大 BUFFER 参数
# 2. 增加 VGROUPS 分散压力
# 3. 检查刷盘是否被阻塞(磁盘 I/O 瓶颈)

性能考量

BUFFER 大小对性能的影响

BUFFER 值 写入效果 内存代价
小(64MB) 频繁刷盘,写入可能被背压阻塞
中(256MB,默认) 均衡 适中
大(1024MB) 长时间缓冲,刷盘频率低

优化建议

场景 建议
高频写入被阻塞 增大 BUFFER 或增加 VGROUPS
内存紧张 减小 BUFFER(但不低于 64MB)
大量乱序数据 保持默认 BUFFER + 增大 STT_TRIGGER
查询 MemTable 慢 检查单 VNode 子表数是否过多

FAQ

Q1: MemTable 满了会丢数据吗?

不会。MemTable 满了时写入会被阻塞(背压),等待刷盘完成释放空间后恢复。期间客户端会收到超时或重试信号。

Q2: 查询时 MemTable 正在刷盘会冲突吗?

不会。Immutable MemTable 是只读的,查询和刷盘可以并发进行。查询读取的是冻结时的快照,不受刷盘写入影响。

Q3: 重启后 MemTable 数据怎么恢复?

通过 WAL 重放。重启时读取 WAL 中 commitIndex 之后的日志,重新写入新的 MemTable,恢复到宕机前的状态。

Q4: 为什么每张子表需要独立的跳表?

因为时间戳在每张子表内是唯一主键。独立跳表保证同表内时间有序,且落盘时可以按表独立处理,避免不同表的数据相互干扰。

参考

系统构架篇

数据模型

存储引擎

相关推荐
-To be number.wan15 小时前
算法日记 | C++ 结构体
数据结构·学习·算法
瀚高PG实验室15 小时前
HGDB安全版单机修改用户密码
数据库·安全·瀚高数据库
CableTech_SQH15 小时前
上海大歌剧院工程综合布线解决方案分析报告
大数据·网络·数据库·5g·信息与通信
CS创新实验室15 小时前
数据结构和算法:摊还分析
java·数据结构·算法
福老板的生意经15 小时前
AI重构短视频营销:一站式创作分发系统的落地场景与商业价值分析
大数据·人工智能
curry____30315 小时前
邻接矩阵 和 领接表 和 链式前向星对比
数据结构·c++·算法
cd_9492172115 小时前
云工场科技推进CPU+GPU协同推理,推动大模型应用降本增效
大数据·人工智能·科技
linmengmeng_131415 小时前
【总结】HugeGraph-AI:当图数据库遇见大模型,构建智能图应用的新范式
数据库·人工智能
是宇写的啊15 小时前
博客系统-小项目
java·数据库·spring boot·mybatis