TDengine 数据缓存架构及使用详解

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 缓存核心优势

  1. 写驱动设计:最新数据自动缓存,命中率 99%+
  2. 多级缓存:写入缓存 + 元数据缓存 + Last 缓存 + 页面缓存
  3. 专用优化:LAST/LAST_ROW 函数直接利用缓存
  4. 配置简单:几个参数即可完成优化
  5. 自动管理:无需应用层处理缓存一致性

性能提升效果

查询类型 无缓存 有缓存 提升
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 提供实时分析、可视化、事件管理与报警等功能。

相关推荐
葡萄城技术团队2 小时前
活字格低代码平台:企业数字化转型的技术架构与实践剖析
低代码·架构
拾忆,想起2 小时前
Dubbo服务依赖问题终结指南:从根因分析到系统化解决方案
微服务·性能优化·架构·dubbo·safari
CNRio2 小时前
Redis:内存中的数据引擎,架构解析与设计指南
数据库·redis·架构
hans汉斯2 小时前
【软件工程与应用】基于大数据的应急救援云平台构建应用研究
大数据·数据库·人工智能·物联网·系统架构·云计算·汉斯出版社
键来大师2 小时前
Android16 RK3576 系统清理缓存
android·缓存·framework·rk3588·android15
Ghost Face...2 小时前
深入解析dd命令:缓存与磁盘速度之谜
linux·缓存
秋刀鱼 ..2 小时前
2026生物神经网络与智能优化国际研讨会(BNNIO 2026)
大数据·python·计算机网络·数学建模·制造
AI优秘企业大脑2 小时前
增长智能体助力企业智慧转型
大数据·人工智能
云边有个稻草人2 小时前
时序数据库选型指南:聚焦IoTDB,适配大数据时代时序数据存储与分析
时序数据库·iotdb