分类 :3.存储引擎 | 篇章:05 Commit 与 Flush

适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-05-30
Commit(提交/落盘)是 MemTable 数据持久化到 TSDB 文件的过程。这是存储引擎中连接内存层和磁盘层的关键环节------既要保证数据不丢失,又要控制写放大、避免阻塞前台写入。
核心概念速查表
| 概念 | 说明 |
|---|---|
| Commit | 将 Immutable MemTable 的数据写入 TSDB 文件 |
| Flush | Commit 的同义词(用户侧命令为 FLUSH DATABASE) |
| Swap | MemTable Active→Immutable 的切换操作 |
| STT 写入 | 直接追加到 STT 文件(快速落盘) |
| 合并写入 | 与已有文件数据合并后写入新文件(有排序开销) |
| 写放大 | 一份数据被反复重写的次数(合并导致) |
详细解析
1. Commit 触发时机
| 触发条件 | 说明 |
|---|---|
| Buffer 段写满 | 当前 Buffer Pool 段使用量达到 BUFFER/3 |
| FLUSH DATABASE 命令 | 用户手动触发 |
| VNode 关闭 | 正常关闭前刷盘未持久化数据 |
| 定时器 | 长时间无写入时也会触发(避免数据长期只在内存) |
2. Commit 全流程
Commit 全流程(8 步):
① Swap 切换:
Active MemTable → Immutable MemTable
新的空 Buffer 段成为 Active
(前台写入继续,不阻塞)
│
▼
② 遍历 Immutable MemTable 中的所有子表数据:
按 RB-Tree 顺序(uid 有序)逐表处理
│
▼
③ 对每张子表的数据:
a. 按时间戳确定目标 FileSet(fid)
b. 将数据按 fid 分组
│
▼
④ 对每个目标 FileSet:
┌─ STT_TRIGGER > 1:
│ 直接写入 STT 文件(追加写)
│ - 构建数据块(编码+压缩)
│ - 追加到 .stt 文件
│ - 更新 STT 块索引
│
└─ STT_TRIGGER = 1:
与已有 .data 文件合并写入新文件
- 读取旧文件相关块
- 合并排序新旧数据
- 写入新 .head + .data + .sma
- 删除旧文件
│
▼
⑤ 生成 .tomb 文件(如果有 DELETE 操作)
│
▼
⑥ 更新 TSDB current 文件(原子更新文件集列表)
│
▼
⑦ 截断已提交的 WAL
│
▼
⑧ 释放 Immutable MemTable 内存
Buffer Pool 段标记为 Free
3. STT 写入模式 vs 合并写入模式
两种落盘策略对比:
STT 模式(STT_TRIGGER > 1):
MemTable 数据 → 直接追加到 STT 文件
优点:
✓ 落盘速度快(纯追加写,无合并开销)
✓ 不读取旧文件(无读放大)
✓ 适合高频写入场景
缺点:
✗ STT 文件累积 → 查询需要多路合并
✗ 需要后台 Compaction 来整理
合并模式(STT_TRIGGER = 1):
MemTable 数据 + 旧文件数据 → 合并排序 → 新文件
优点:
✓ 数据始终有序(查询无需多路合并)
✓ 无 STT 堆积
缺点:
✗ 落盘需要读旧文件(读放大)
✗ 写放大高(旧数据被重写)
✗ 落盘延迟高
4. 数据块构建过程
从 MemTable 行数据到磁盘数据块的转换:
MemTable 中的行数据(行式,未压缩)
│
▼
① 行转列:将 N 行数据拆分为各列的值数组
timestamp[] = [t1, t2, t3, ...]
col1[] = [v1, v2, v3, ...]
col2[] = [v1, v2, v3, ...]
│
▼
② 构建 NULL Bitmap:标记每列的 NULL 位置
│
▼
③ 类型特化编码(一阶压缩):
- TIMESTAMP: Delta-of-Delta
- INT/BIGINT: ZigZag + Simple8B
- FLOAT/DOUBLE: XOR 差分
- BINARY: 字典编码
│
▼
④ 通用压缩(二阶压缩,COMP=2 时):
- LZ4 或 ZSTD 对编码后的字节流压缩
│
▼
⑤ 组装数据块:
Block Header + Column Metadata + Column Data
│
▼
⑥ 写入 .data 文件,记录偏移到 .head
5. 多 FileSet 的落盘分发
乱序数据导致跨 FileSet 落盘:
假设 DURATION = 10 天,当前时间在 FileSet 3
MemTable 中的数据:
- 99% 数据时间戳在 FileSet 3 范围内(正常)
- 1% 数据时间戳在 FileSet 2 范围内(乱序/补报)
Commit 时:
FileSet 3: 接收 99% 数据 → 追加/合并
FileSet 2: 接收 1% 数据 → 合并到已有文件(或追加 STT)
乱序数据对 Commit 的额外开销:
- 需要读取旧 FileSet 的索引
- 可能触发旧 FileSet 的 Compaction
6. 原子性保证
Commit 的原子性:
TSDB 使用 "write-new-then-switch" 策略:
① 写入新文件(新的 .head/.data/.sma)
② 所有文件写完后 → fsync 确保持久化
③ 原子更新 current 文件(切换文件集指针)
④ 删除旧文件(如果是合并写入)
崩溃恢复:
- 如果在 ③ 之前崩溃 → 新文件被忽略(垃圾回收)
- 如果在 ③ 之后崩溃 → 新文件生效
- 不会出现半写入状态
7. Commit 期间的读写并发
Commit 与读写的并发关系:
写入操作:
→ 写入新的 Active MemTable(与 Commit 不冲突)
查询操作:
→ 读 Active MemTable ✓
→ 读 Immutable MemTable ✓(只读,安全并发)
→ 读磁盘文件 ✓(Commit 写新文件,不修改旧文件)
Commit 操作:
→ 读 Immutable MemTable(遍历数据)
→ 写新磁盘文件
结论:前台读写与后台 Commit 完全并发,互不阻塞
8. FLUSH DATABASE 命令
sql
-- 手动触发所有 VNode 的 Commit
FLUSH DATABASE power;
-- 使用场景:
-- 1. 维护前确保所有数据已落盘
-- 2. 测试/验证数据持久化
-- 3. 调试:确认数据已写入文件
代码示例
监控 Commit 状态
bash
# 观察 taosd 日志中的 Commit 相关信息
grep -i "commit" /var/log/taos/taosdlog.0
# 典型日志:
# "vgId:2 start to commit, nRows:100000"
# "vgId:2 commit finished, elapsed:500ms"
调优参数
sql
-- 减少 Commit 频率(增大 Buffer)
ALTER DATABASE power BUFFER 512;
-- 加快落盘速度(减少合并开销)
ALTER DATABASE power STT_TRIGGER 4;
-- 查看当前配置
SELECT * FROM information_schema.ins_databases WHERE name='power';
性能考量
Commit 耗时分析
| 阶段 | 典型耗时 | 瓶颈 |
|---|---|---|
| Swap MemTable | < 1ms | CPU(指针交换) |
| 遍历 MemTable | 10~100ms | CPU + 内存带宽 |
| 编码压缩 | 50~500ms | CPU |
| 磁盘写入 | 100~2000ms | 磁盘 IOPS/带宽 |
| fsync | 0.1~10ms | 磁盘 |
| WAL 截断 | < 10ms | 磁盘 |
STT_TRIGGER 对性能的综合影响
| STT_TRIGGER | 写入延迟 | 查询延迟 | 写放大 | 磁盘 I/O |
|---|---|---|---|---|
| 1 | 高(合并写) | 最低 | 高 | 高 |
| 2(默认) | 中 | 低 | 中 | 中 |
| 4 | 低 | 中 | 低 | 低 |
| 8~16 | 最低 | 高(多路合并) | 最低 | 最低 |
FAQ
Q1: Commit 期间写入被阻塞了怎么办?
说明所有 Buffer Pool 段都被占满(一个在 Commit,另一个已满无法轮转)。解决:
- 增大 BUFFER 参数(增加缓冲空间)
- 检查磁盘 I/O 是否饱和(Commit 写盘慢)
- 增加 VGROUPS 分散单 VNode 的写入压力
Q2: FLUSH DATABASE 是同步还是异步?
异步。命令发出后立即返回,实际 Commit 在后台线程执行。如果需要确认 Flush 完成,可以通过日志观察或等待一段时间后检查文件。
Q3: 乱序数据多时 Commit 会变慢吗?
会。乱序数据需要写入旧的 FileSet,可能需要:
- 读取旧文件的块索引
- 与旧数据合并排序
- 重写旧文件
如果乱序比例高,建议增大 STT_TRIGGER 让数据先追加到 STT,后台 Compaction 异步整理。
Q4: Commit 失败会丢数据吗?
不会。Immutable MemTable 的数据在 Commit 完成前不会释放,且 WAL 中仍有完整日志。如果 Commit 失败(如磁盘满),系统会重试。WAL 是最终的数据安全保障。
参考
系统构架篇
- 01-《TDengine 整体架构全景》
- 02-《集群拓扑深度解析》
- 03-《MNode 内部机制深度解析》
- 04-《RPC 通信层深度解析》
- 05-《VNode 生命周期》
- 06-《RAFT 共识协议》
- 07-《端到端的消息流》
数据模型
- 01-《数据库创建与参数详解》
- 02-《超级表/子表/普通表》
- 03-《支持数据类型深度解析》
- 04-《TDengine Tag 设计哲学与 Schema 变更机制》
- 05-《TDengine 虚拟表实现原理》
存储引擎
关于 TDengine
TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。