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: 为什么每张子表需要独立的跳表?

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

参考

系统构架篇

数据模型

存储引擎

相关推荐
神威难绷泪2 分钟前
数据结构:哈希表 算法相关 排序算法
数据结构
陈聪.15 分钟前
企业级 NoSQL 数据库 Redis 核心知识整理
数据库·redis·nosql
智能运维指南19 分钟前
智能体自治运维选型指南:2026年企业如何从“AI辅助”走向“AI自治”
大数据·运维·人工智能
翼龙云_cloud26 分钟前
腾讯云国际代理商:CDB数据库自动备份和异地灾备配置 从快照到跨区域恢复
运维·数据库·云计算·腾讯云
小屁孩你滑稽掉了26 分钟前
prisma操作数据库的方法使用教程(简洁版)
数据库·node.js·prisma·fastify
迅易科技36 分钟前
从数据可视化到运营闭环,制造企业如何构建卓越运营数字化体系?
大数据·制造·big data
LHX sir1 小时前
医疗设备外设多、协议杂?HubPort 让出厂设备自带 AI
人工智能·物联网·医疗设备·设备智能化
l1t1 小时前
利用DuckDB luajit插件和openblas库对表中数据做矩阵运算
数据库·线性代数·矩阵·duckdb
Elastic 中国社区官方博客1 小时前
Elasticsearch:什么是向量数据库?
大数据·运维·数据库·人工智能·elasticsearch·搜索引擎·ai
久美子1 小时前
铁轨、迷宫与护栏:AI 数据工程的确定性骨架
大数据