
TDengine 数据缓存架构及使用详解
一、设计理念
TDengine 采用**写驱动缓存(Write-driven Cache)**设计,与传统数据库的读驱动缓存截然不同。核心思想是:时序数据场景下,最新写入的数据往往是最频繁查询的数据。
传统缓存 vs TDengine 缓存
传统数据库(读驱动缓存):
查询什么 → 缓存什么
↓
问题:冷数据占用缓存空间
问题:缓存命中率不稳定
TDengine(写驱动缓存):
写入什么 → 缓存什么
↓
优势:最新数据始终在缓存
优势:最新数据查询命中率 99%+
优势:可替代 Redis 等缓存中间件
二、缓存架构总览
┌─────────────────────────────────────────────────────────┐
│ TDengine 多级缓存架构 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ L1: 写入缓存 (Write Buffer) │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │ Block1 │ │ Block2 │ │ Block3 │ (轮转使用) │ │
│ │ │(写入中)│ │(待落盘)│ │(落盘中)│ │ │
│ │ └────────┘ └────────┘ └────────┘ │ │
│ │ 数据结构: SkipList + Red-Black Tree │ │
│ │ 作用: 接收写入 + 缓存最新数据 │ │
│ └────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────┐ │
│ │ L2: 元数据缓存 (Meta Cache) │ │
│ │ - 表 Schema 信息 │ │
│ │ - 标签信息 │ │
│ │ - vgroup 路由信息 │ │
│ │ 数据结构: B+Tree + LRU │ │
│ │ 作用: 加速表查找和标签过滤 │ │
│ └────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────┐ │
│ │ L3: Last/Last_Row 缓存 │ │
│ │ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ Last 缓存 │ │ Last_Row 缓存 │ │ │
│ │ │ (每列最新值) │ │ (最后一条记录) │ │ │
│ │ └─────────────┘ └─────────────────┘ │ │
│ │ 数据结构: LRU + 延迟加载 │ │
│ │ 作用: 加速 LAST/LAST_ROW 查询 │ │
│ └────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────┐ │
│ │ L4: 页面缓存 (Page Cache) │ │
│ │ - 数据页缓存 │ │
│ │ - 索引页缓存 │ │
│ │ 数据结构: Hash + LRU │ │
│ │ 作用: 加速磁盘数据访问 │ │
│ └────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
三、核心缓存机制详解
3.1 写入缓存 (Write Buffer)
设计原理:采用多块内存轮转机制,实现写入与落盘并行
内存块轮转机制:
时刻 T1:
[Block1: 写入中] → [Block2: 空闲] → [Block3: 空闲]
时刻 T2 (Block1 写满 1/3):
[Block1: 待落盘] → [Block2: 写入中] → [Block3: 空闲]
↓
触发 Block1 落盘(后台线程)
时刻 T3 (Block1 落盘完成):
[Block1: 空闲] → [Block2: 写入中] → [Block3: 空闲]
优势:
✓ 写入永不阻塞
✓ 始终保持 1/3 内存缓存最新数据
✓ 落盘与写入并行执行
源码实现(基于 SkipList):
c
// 数据写入流程
写入数据
↓
查找目标表的 SkipList(O(log n))
↓
插入数据到 SkipList(按时间排序)
↓
更新统计信息
↓
检查内存块使用率
↓
超过阈值 → 触发落盘
配置参数:
sql
-- 创建数据库时配置写入缓存
CREATE DATABASE sensor_db
BUFFER 256 -- 每个 vnode 的写缓存大小(MB)
PAGES 128 -- 元数据缓存页数
PAGESIZE 4; -- 每页大小(KB)
-- 参数说明:
-- BUFFER: 写入缓存总大小,越大可缓存越多最新数据
-- PAGES × PAGESIZE = 元数据缓存大小
3.2 Last/Last_Row 缓存
设计原理:专门为时序数据的"查询最新值"场景优化
Last 缓存:
┌─────────────────────────────────────────┐
│ sensor_001: │
│ temperature: (ts=2024-01-15 12:00, 25.5)│
│ humidity: (ts=2024-01-15 12:00, 60) │
│ pressure: (ts=2024-01-15 11:55, 101) │ ← 各列可能时间不同
└─────────────────────────────────────────┘
Last_Row 缓存:
┌─────────────────────────────────────────┐
│ sensor_001: │
│ 最后一行: (ts=2024-01-15 12:00, │
│ temperature=25.5, │
│ humidity=60, │
│ pressure=NULL) ← 该行可能有 NULL│
└─────────────────────────────────────────┘
区别:
- LAST(col): 返回每列最后一个非 NULL 值(时间可能不同)
- LAST_ROW(*): 返回表中最后一条记录(可能含 NULL)
配置方式:
sql
-- 方式1: 创建数据库时配置
CREATE DATABASE sensor_db
CACHEMODEL 'both' -- 启用 last 和 last_row 缓存
CACHESIZE 10; -- 缓存大小(MB)
-- CACHEMODEL 选项:
-- 'none': 不启用缓存
-- 'last_row': 只缓存 last_row
-- 'last_value': 只缓存 last
-- 'both': 同时缓存两者(推荐)
-- 方式2: 修改已有数据库
ALTER DATABASE sensor_db CACHEMODEL 'both';
ALTER DATABASE sensor_db CACHESIZE 20;
查询加速效果:
sql
-- 无缓存时
SELECT LAST(temperature) FROM sensor_001;
执行: 扫描数据文件 → 找到最后非 NULL 值
耗时: 10-100ms
-- 有缓存时
SELECT LAST(temperature) FROM sensor_001;
执行: 直接从 Last 缓存返回
耗时: < 1ms
性能提升: 100x+
3.3 元数据缓存 (Meta Cache)
设计原理:基于 B+Tree + LRU 的元数据管理
c
// 源码结构 (tdbPCache.c)
struct SPCache {
int szPage; // 页面大小
int nPages; // 总页面数
SPage **aPage; // 页面数组
tdb_mutex_t mutex; // 并发锁
int nFree; // 空闲页面数
SPage *pFree; // 空闲链表
int nHash; // 哈希表大小
SPage **pgHash; // 页面哈希表
int nRecyclable; // 可回收页面数
SPage lru; // LRU 链表头
};
缓存页面生命周期:
页面状态流转:
[空闲列表] → 分配 → [活跃使用]
↓ 引用计数归零
[LRU 队列]
↓ 空间不足时回收
[空闲列表]
关键操作:
1. Fetch: 从缓存获取页面(命中)或从磁盘加载
2. Pin: 固定页面,防止被回收
3. Unpin: 释放固定,允许回收
4. Release: 减少引用计数
3.4 页面缓存实现细节
哈希查找 + LRU 淘汰:
c
// 页面查找流程 (简化)
SPage* tdbPCacheFetchImpl(SPCache *pCache, const SPgid *pPgid) {
// 1. 计算哈希值
uint32_t h = tdbPCachePageHash(pPgid) % pCache->nHash;
// 2. 在哈希表中查找
SPage *pPage = pCache->pgHash[h];
while (pPage) {
if (pPage->pgid.pgno == pPgid->pgno &&
memcmp(pPage->pgid.fileid, pPgid->fileid, TDB_FILE_ID_LEN) == 0)
break;
pPage = pPage->pHashNext;
}
// 3. 命中:从 LRU 移除(Pin 住)
if (pPage) {
tdbPCachePinPage(pCache, pPage);
return pPage;
}
// 4. 未命中:从空闲列表或 LRU 获取页面
if (pCache->pFree) {
pPage = pCache->pFree;
pCache->pFree = pPage->pFreeNext;
} else if (!pCache->lru.pLruPrev->isAnchor) {
// 从 LRU 尾部回收
pPage = pCache->lru.pLruPrev;
tdbPCacheRemovePageFromHash(pCache, pPage);
}
// 5. 加载数据并加入哈希表
// ...
return pPage;
}
四、缓存优化最佳实践
4.1 ✅ 推荐配置
场景1:高频实时查询(IoT 监控)
sql
-- 最新数据查询频繁,需要大缓存
CREATE DATABASE iot_monitor
BUFFER 512 -- 大写入缓存(512MB)
CACHEMODEL 'both' -- 启用双缓存
CACHESIZE 100 -- Last 缓存 100MB
PAGES 256 -- 元数据缓存页数
PAGESIZE 4; -- 4KB 页面
-- 适用场景:
-- - 设备状态实时监控
-- - 最新数据仪表盘展示
-- - 实时告警系统
场景2:历史数据分析(数据仓库)
sql
-- 历史查询为主,缓存可以小一些
CREATE DATABASE data_warehouse
BUFFER 128 -- 中等写入缓存
CACHEMODEL 'none' -- 不需要 Last 缓存
PAGES 512 -- 大元数据缓存(历史表多)
PAGESIZE 4;
-- 适用场景:
-- - 历史数据报表
-- - 批量数据分析
-- - 数据归档存储
场景3:混合负载(通用场景)
sql
-- 平衡配置
CREATE DATABASE mixed_workload
BUFFER 256
CACHEMODEL 'both'
CACHESIZE 50
PAGES 256
PAGESIZE 4;
4.2 ✅ 加速查询的技巧
1. 利用 LAST_ROW 替代 ORDER BY LIMIT
sql
-- ❌ 慢:需要排序
SELECT * FROM sensor_001
ORDER BY ts DESC
LIMIT 1;
-- ✅ 快:直接从缓存返回
SELECT LAST_ROW(*) FROM sensor_001;
性能提升: 100x+
2. 使用 LAST 获取各列最新值
sql
-- ❌ 慢:多次查询
SELECT (SELECT temperature FROM sensor_001 ORDER BY ts DESC LIMIT 1) as temp,
(SELECT humidity FROM sensor_001 ORDER BY ts DESC LIMIT 1) as hum;
-- ✅ 快:一次查询,命中缓存
SELECT LAST(temperature), LAST(humidity) FROM sensor_001;
性能提升: 50x+
3. 批量获取多表最新值
sql
-- ✅ 高效:利用 Last 缓存
SELECT tbname, LAST(temperature), LAST(humidity)
FROM sensors
WHERE location = '北京'
GROUP BY tbname;
-- 执行过程:
-- 1. 标签过滤(内存操作,毫秒级)
-- 2. 每张表从 Last 缓存获取值
-- 3. 组装返回结果
-- 1000 张表查询耗时: < 100ms
4. 实时数据展示优化
sql
-- ✅ 推荐:最近 N 分钟数据(命中写入缓存)
SELECT * FROM sensor_001
WHERE ts >= NOW - 10m;
-- 执行过程:
-- 1. 检查写入缓存(MemTable)
-- 2. 大部分数据在内存中命中
-- 3. 少量数据可能需要读磁盘
-- 缓存命中率: 90%+
4.3 ❌ 要避免的误区
1. 避免不合理的缓存配置
sql
-- ❌ 差:缓存太小,写入频繁落盘
CREATE DATABASE sensor_db BUFFER 16;
-- 问题:
-- - 频繁触发落盘
-- - 写入性能下降
-- - 最新数据缓存命中率低
-- ✅ 好:根据写入量配置
-- 计算公式: BUFFER >= 写入速率 × 期望缓存时间
-- 例如: 10MB/s × 30s = 300MB
CREATE DATABASE sensor_db BUFFER 300;
2. 避免未启用 Last 缓存却频繁查询最新值
sql
-- ❌ 差:未启用缓存
CREATE DATABASE sensor_db CACHEMODEL 'none';
-- 然后频繁执行
SELECT LAST(temperature) FROM sensor_001; -- 每次都读磁盘
-- ✅ 好:启用缓存
ALTER DATABASE sensor_db CACHEMODEL 'both';
3. 避免 CACHESIZE 设置过小
sql
-- ❌ 差:缓存太小,频繁淘汰
CREATE DATABASE sensor_db
CACHEMODEL 'both'
CACHESIZE 1; -- 只有 1MB
-- 问题:
-- - 10000 张表,每表缓存条目约 100 字节
-- - 1MB 只能缓存约 10000 条
-- - 频繁 LRU 淘汰,缓存命中率低
-- ✅ 好:根据表数量配置
-- 计算公式: CACHESIZE >= 表数量 × 0.001 MB
-- 例如: 100000 张表 → CACHESIZE 100
CREATE DATABASE sensor_db
CACHEMODEL 'both'
CACHESIZE 100;
4. 避免用 SELECT * 查询最新数据
sql
-- ❌ 差:SELECT * 可能不走缓存
SELECT * FROM sensor_001 ORDER BY ts DESC LIMIT 1;
-- ✅ 好:使用专用函数
SELECT LAST_ROW(*) FROM sensor_001;
-- 或
SELECT LAST(col1), LAST(col2), ... FROM sensor_001;
五、性能监控与调优
5.1 查看缓存配置
sql
-- 查看数据库缓存配置
SHOW DATABASES;
-- 查看详细参数
SELECT * FROM information_schema.ins_databases
WHERE name = 'sensor_db';
-- 关注字段:
-- buffer: 写入缓存大小
-- cachemodel: 缓存模式
-- cachesize: Last 缓存大小
-- pages: 元数据缓存页数
5.2 动态调整缓存
sql
-- 增大 Last 缓存
ALTER DATABASE sensor_db CACHESIZE 200;
-- 修改缓存模式
ALTER DATABASE sensor_db CACHEMODEL 'both';
-- 注意: BUFFER 和 PAGES 创建后不可修改
5.3 内存使用估算
总内存使用 ≈
vnode数 × BUFFER
+ vnode数 × PAGES × PAGESIZE
+ CACHESIZE
+ 系统开销
示例计算:
- 4 个 vnode
- BUFFER = 256MB
- PAGES = 128, PAGESIZE = 4KB
- CACHESIZE = 50MB
总计 ≈ 4×256 + 4×128×0.004 + 50 + 100
≈ 1024 + 2 + 50 + 100
≈ 1176 MB
六、缓存与其他数据库对比
| 特性 | TDengine | MySQL | Redis |
|---|---|---|---|
| 缓存驱动 | 写驱动 | 读驱动 | 读驱动 |
| 最新数据命中 | 99%+ | 不确定 | 需要应用层维护 |
| 专用函数 | LAST/LAST_ROW | 无 | 无 |
| 缓存一致性 | 自动保证 | 需要应用层处理 | 需要应用层处理 |
| 内存效率 | 高 | 中 | 高 |
| 配置复杂度 | 低 | 中 | 高 |
七、总结
TDengine 缓存核心优势
- ✅ 写驱动设计:最新数据自动缓存,命中率 99%+
- ✅ 多级缓存:写入缓存 + 元数据缓存 + Last 缓存 + 页面缓存
- ✅ 专用优化:LAST/LAST_ROW 函数直接利用缓存
- ✅ 配置简单:几个参数即可完成优化
- ✅ 自动管理:无需应用层处理缓存一致性
性能提升效果
| 查询类型 | 无缓存 | 有缓存 | 提升 |
|---|---|---|---|
| LAST_ROW | 10-50ms | < 1ms | 100x+ |
| LAST | 10-100ms | < 1ms | 100x+ |
| 最近时间范围 | 50-200ms | 5-20ms | 10x+ |
| 元数据查询 | 10-50ms | < 1ms | 50x+ |
最佳实践速查
sql
-- 高频实时查询场景
CREATE DATABASE iot_db
BUFFER 512
CACHEMODEL 'both'
CACHESIZE 100
PAGES 256
PAGESIZE 4;
-- 查询最新数据
SELECT LAST_ROW(*) FROM table_name;
SELECT LAST(col1), LAST(col2) FROM table_name;
-- 查询最近数据(命中写入缓存)
SELECT * FROM table_name WHERE ts >= NOW - 10m;
TDengine 的缓存机制是其高性能的关键因素之一,通过合理配置和正确使用,可以实现毫秒级最新数据查询,大幅提升时序数据应用的用户体验。
关于 TDengine
TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。