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):
// 数据写入流程写入数据 ↓ 查找目标表的 SkipList(O(log n)) ↓ 插入数据到 SkipList(按时间排序) ↓ 更新统计信息 ↓ 检查内存块使用率 ↓ 超过阈值 → 触发落盘配置参数:
-- 创建数据库时配置写入缓存CREATEDATABASEsensor_db BUFFER256-- 每个 vnode 的写缓存大小(MB)PAGES128-- 元数据缓存页数PAGESIZE4;-- 每页大小(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)配置方式:
-- 方式1: 创建数据库时配置CREATEDATABASEsensor_db CACHEMODEL'both'-- 启用 last 和 last_row 缓存CACHESIZE10;-- 缓存大小(MB)-- CACHEMODEL 选项:-- 'none': 不启用缓存-- 'last_row': 只缓存 last_row-- 'last_value': 只缓存 last-- 'both': 同时缓存两者(推荐)-- 方式2: 修改已有数据库ALTERDATABASEsensor_db CACHEMODEL'both';ALTERDATABASEsensor_db CACHESIZE20;查询加速效果:
-- 无缓存时SELECTLAST(temperature)FROMsensor_001;执行: 扫描数据文件 → 找到最后非NULL值 耗时:10-100ms-- 有缓存时SELECTLAST(temperature)FROMsensor_001;执行: 直接从Last缓存返回 耗时:<1ms 性能提升:100x+3.3 元数据缓存 (Meta Cache)
设计原理:基于 B+Tree + LRU 的元数据管理
// 源码结构 (tdbPCache.c)structSPCache{intszPage;// 页面大小intnPages;// 总页面数SPage**aPage;// 页面数组tdb_mutex_tmutex;// 并发锁intnFree;// 空闲页面数SPage*pFree;// 空闲链表intnHash;// 哈希表大小SPage**pgHash;// 页面哈希表intnRecyclable;// 可回收页面数SPage lru;// LRU 链表头};缓存页面生命周期:
页面状态流转: [空闲列表] → 分配 → [活跃使用] ↓ 引用计数归零 [LRU 队列] ↓ 空间不足时回收 [空闲列表] 关键操作: 1. Fetch: 从缓存获取页面(命中)或从磁盘加载 2. Pin: 固定页面,防止被回收 3. Unpin: 释放固定,允许回收 4. Release: 减少引用计数3.4 页面缓存实现细节
哈希查找 + LRU 淘汰:
// 页面查找流程 (简化)SPage*tdbPCacheFetchImpl(SPCache*pCache,constSPgid*pPgid){// 1. 计算哈希值uint32_th=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);returnpPage;}// 4. 未命中:从空闲列表或 LRU 获取页面if(pCache->pFree){pPage=pCache->pFree;pCache->pFree=pPage->pFreeNext;}elseif(!pCache->lru.pLruPrev->isAnchor){// 从 LRU 尾部回收pPage=pCache->lru.pLruPrev;tdbPCacheRemovePageFromHash(pCache,pPage);}// 5. 加载数据并加入哈希表// ...returnpPage;}四、缓存优化最佳实践
4.1 ✅ 推荐配置
场景1:高频实时查询(IoT 监控)
-- 最新数据查询频繁,需要大缓存CREATEDATABASEiot_monitor BUFFER512-- 大写入缓存(512MB)CACHEMODEL'both'-- 启用双缓存CACHESIZE100-- Last 缓存 100MBPAGES256-- 元数据缓存页数PAGESIZE4;-- 4KB 页面-- 适用场景:-- - 设备状态实时监控-- - 最新数据仪表盘展示-- - 实时告警系统场景2:历史数据分析(数据仓库)
-- 历史查询为主,缓存可以小一些CREATEDATABASEdata_warehouse BUFFER128-- 中等写入缓存CACHEMODEL'none'-- 不需要 Last 缓存PAGES512-- 大元数据缓存(历史表多)PAGESIZE4;-- 适用场景:-- - 历史数据报表-- - 批量数据分析-- - 数据归档存储场景3:混合负载(通用场景)
-- 平衡配置CREATEDATABASEmixed_workload BUFFER256CACHEMODEL'both'CACHESIZE50PAGES256PAGESIZE4;4.2 ✅ 加速查询的技巧
1. 利用 LAST_ROW 替代 ORDER BY LIMIT
-- ❌ 慢:需要排序SELECT*FROMsensor_001ORDERBYtsDESCLIMIT1;-- ✅ 快:直接从缓存返回SELECTLAST_ROW(*)FROMsensor_001;性能提升:100x+2. 使用 LAST 获取各列最新值
-- ❌ 慢:多次查询SELECT(SELECTtemperatureFROMsensor_001ORDERBYtsDESCLIMIT1)astemp,(SELECThumidityFROMsensor_001ORDERBYtsDESCLIMIT1)ashum;-- ✅ 快:一次查询,命中缓存SELECTLAST(temperature),LAST(humidity)FROMsensor_001;性能提升:50x+3. 批量获取多表最新值
-- ✅ 高效:利用 Last 缓存SELECTtbname,LAST(temperature),LAST(humidity)FROMsensorsWHERElocation='北京'GROUPBYtbname;-- 执行过程:-- 1. 标签过滤(内存操作,毫秒级)-- 2. 每张表从 Last 缓存获取值-- 3. 组装返回结果-- 1000 张表查询耗时: < 100ms4. 实时数据展示优化
-- ✅ 推荐:最近 N 分钟数据(命中写入缓存)SELECT*FROMsensor_001WHEREts>=NOW-10m;-- 执行过程:-- 1. 检查写入缓存(MemTable)-- 2. 大部分数据在内存中命中-- 3. 少量数据可能需要读磁盘-- 缓存命中率: 90%+4.3 ❌ 要避免的误区
1. 避免不合理的缓存配置
-- ❌ 差:缓存太小,写入频繁落盘CREATEDATABASEsensor_db BUFFER16;-- 问题:-- - 频繁触发落盘-- - 写入性能下降-- - 最新数据缓存命中率低-- ✅ 好:根据写入量配置-- 计算公式: BUFFER >= 写入速率 × 期望缓存时间-- 例如: 10MB/s × 30s = 300MBCREATEDATABASEsensor_db BUFFER300;2. 避免未启用 Last 缓存却频繁查询最新值
-- ❌ 差:未启用缓存CREATEDATABASEsensor_db CACHEMODEL'none';-- 然后频繁执行SELECTLAST(temperature)FROMsensor_001;-- 每次都读磁盘-- ✅ 好:启用缓存ALTERDATABASEsensor_db CACHEMODEL'both';3. 避免 CACHESIZE 设置过小
-- ❌ 差:缓存太小,频繁淘汰CREATEDATABASEsensor_db CACHEMODEL'both'CACHESIZE1;-- 只有 1MB-- 问题:-- - 10000 张表,每表缓存条目约 100 字节-- - 1MB 只能缓存约 10000 条-- - 频繁 LRU 淘汰,缓存命中率低-- ✅ 好:根据表数量配置-- 计算公式: CACHESIZE >= 表数量 × 0.001 MB-- 例如: 100000 张表 → CACHESIZE 100CREATEDATABASEsensor_db CACHEMODEL'both'CACHESIZE100;4. 避免用 SELECT * 查询最新数据
-- ❌ 差:SELECT * 可能不走缓存SELECT*FROMsensor_001ORDERBYtsDESCLIMIT1;-- ✅ 好:使用专用函数SELECTLAST_ROW(*)FROMsensor_001;-- 或SELECTLAST(col1),LAST(col2),...FROMsensor_001;五、性能监控与调优
5.1 查看缓存配置
-- 查看数据库缓存配置SHOWDATABASES;-- 查看详细参数SELECT*FROMinformation_schema.ins_databasesWHEREname='sensor_db';-- 关注字段:-- buffer: 写入缓存大小-- cachemodel: 缓存模式-- cachesize: Last 缓存大小-- pages: 元数据缓存页数5.2 动态调整缓存
-- 增大 Last 缓存ALTERDATABASEsensor_db CACHESIZE200;-- 修改缓存模式ALTERDATABASEsensor_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+ |
最佳实践速查
-- 高频实时查询场景CREATEDATABASEiot_db BUFFER512CACHEMODEL'both'CACHESIZE100PAGES256PAGESIZE4;-- 查询最新数据SELECTLAST_ROW(*)FROMtable_name;SELECTLAST(col1),LAST(col2)FROMtable_name;-- 查询最近数据(命中写入缓存)SELECT*FROMtable_nameWHEREts>=NOW-10m;TDengine 的缓存机制是其高性能的关键因素之一,通过合理配置和正确使用,可以实现毫秒级最新数据查询,大幅提升时序数据应用的用户体验。
关于 TDengine
TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。