TDengine Cache 与 Last 查询加速 — CACHEMODEL 机制与 RocksDB 缓存层

分类 :3.存储引擎 | 篇章:09 Cache 与 Last 查询

适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-05-18

在物联网场景中,"查询每个设备的最新值"是最高频的操作之一。TDengine 通过 CACHEMODEL 机制在 VNode 内维护一套基于 RocksDB 的独立缓存层,让 LAST()LAST_ROW() 查询无需扫描 TSDB 数据文件,直接从缓存返回结果。

核心概念速查表

概念 说明
CACHEMODEL 缓存模式(none / last_row / last_value / both)
CACHESIZE 每个 VNode 的缓存大小上限(MB)
LAST_ROW() 返回最后一行(不忽略 NULL)
LAST() 返回每列最后一个非 NULL 值
cache.rdb RocksDB 持久化缓存文件
缓存 Key (uid, column_id, lflag) 三元组

详细解析

1. CACHEMODEL 四种模式

复制代码
CACHEMODEL 配置:

  ┌─────────────────────────────────────────────────────┐
  │  none(默认)                                        │
  │    不维护任何缓存                                     │
  │    LAST()/LAST_ROW() → 需要扫描 TSDB 数据文件       │
  │    适用:不常查最新值的场景                           │
  └─────────────────────────────────────────────────────┘
  
  ┌─────────────────────────────────────────────────────┐
  │  last_row                                            │
  │    只缓存每张子表的最后一行                           │
  │    LAST_ROW() → 从缓存直接返回(极快)               │
  │    LAST() → 仍需扫描文件(因为要找非NULL值)         │
  └─────────────────────────────────────────────────────┘
  
  ┌─────────────────────────────────────────────────────┐
  │  last_value                                          │
  │    缓存每列的最后一个非 NULL 值                       │
  │    LAST() → 从缓存直接返回                           │
  │    LAST_ROW() → 仍需扫描文件                         │
  └─────────────────────────────────────────────────────┘
  
  ┌─────────────────────────────────────────────────────┐
  │  both                                                │
  │    同时缓存 last_row 和 last_value                   │
  │    LAST()/LAST_ROW() 都从缓存直接返回                │
  │    内存消耗最大,但查询最快                           │
  └─────────────────────────────────────────────────────┘

2. LAST_ROW 与 LAST 的区别

复制代码
数据示例:

  子表 d1001 的最近 5 行:
  ┌─────────┬────────────┬──────────┬──────────┐
  │  ts     │ temperature│ humidity │ status   │
  ├─────────┼────────────┼──────────┼──────────┤
  │  T1     │  25.3      │  60      │  'OK'    │
  │  T2     │  25.4      │  NULL    │  'OK'    │
  │  T3     │  NULL      │  62      │  NULL    │
  │  T4     │  25.5      │  NULL    │  NULL    │
  │  T5     │  NULL      │  NULL    │  'ERR'   │  ← 最后一行
  └─────────┴────────────┴──────────┴──────────┘

  LAST_ROW(temperature, humidity, status):
    → (NULL, NULL, 'ERR')  ← 最后一行的值,不管是否为 NULL

  LAST(temperature, humidity, status):
    → (25.5, 62, 'ERR')   ← 每列最后一个非 NULL 值
    (temperature 取 T4 的 25.5, humidity 取 T3 的 62)

3. 缓存存储结构

复制代码
缓存的物理存储(RocksDB):

  每个 VNode 下:
    cache/
    └── cache.rdb         ← RocksDB 数据库文件
    
  Key 结构: (uid, column_id, lflag)
    - uid: 子表的唯一 ID
    - column_id: 列编号
    - lflag: 0=last_row, 1=last_value
    
  Value 结构: (timestamp, value)
    - timestamp: 该值对应的时间戳
    - value: 实际数据值
    
    
  示例缓存内容(子表 uid=1001, 3列):
  
  Key                    │ Value
  ───────────────────────┼──────────────────
  (1001, 0, 0)          │ (T5, T5)           ← last_row 的 ts 列
  (1001, 1, 0)          │ (T5, NULL)         ← last_row 的 col1
  (1001, 2, 0)          │ (T5, NULL)         ← last_row 的 col2
  (1001, 3, 0)          │ (T5, 'ERR')        ← last_row 的 col3
  (1001, 1, 1)          │ (T4, 25.5)         ← last_value 的 col1
  (1001, 2, 1)          │ (T3, 62)           ← last_value 的 col2
  (1001, 3, 1)          │ (T5, 'ERR')        ← last_value 的 col3

4. 缓存更新机制

复制代码
写入时的缓存更新(Write-Through):

  数据写入 MemTable 的同时,同步更新 Cache:
  
  写入新行 (T6, 26.0, NULL, 'OK')
       │
       ├──→ 写入 MemTable(正常路径)
       │
       └──→ 更新缓存:
            last_row:
              所有列更新为 T6 行的值
              (1001, 1, 0) → (T6, 26.0)
              (1001, 2, 0) → (T6, NULL)
              (1001, 3, 0) → (T6, 'OK')
              
            last_value:
              只更新非 NULL 的列:
              (1001, 1, 1) → (T6, 26.0)  ✓ 非NULL,更新
              (1001, 2, 1) → 不更新       ✗ 写入值为NULL
              (1001, 3, 1) → (T6, 'OK')  ✓ 非NULL,更新


  删除时的缓存失效:
  
  DELETE FROM d1001 WHERE ts >= T5
       │
       └──→ 缓存可能失效
            → 需要回扫 TSDB 确定新的 last 值
            → 异步重建缓存

5. 缓存与查询加速

复制代码
LAST() 查询的执行路径对比:

  无缓存(CACHEMODEL=none):
    SELECT LAST(temperature) FROM d1001
       │
       ▼
    打开 MemTable → 扫描最后几个块 → 找非 NULL 值
    打开最新 FileSet → 从后往前读数据块
    → 可能需要读取多个块直到找到非 NULL
    延迟:10~100ms
    
    
  有缓存(CACHEMODEL=both):
    SELECT LAST(temperature) FROM d1001
       │
       ▼
    查询 RocksDB: Key=(uid_d1001, col_temperature, 1)
    → 直接返回缓存值
    延迟:< 1ms
    
    
  海量子表场景的加速效果:
    SELECT LAST(temperature) FROM meters GROUP BY tbname
    
    无缓存:逐表扫描文件 → N × 10ms
    有缓存:逐表查缓存 → N × 0.01ms
    加速比:100~1000 倍

6. CACHESIZE 参数

复制代码
CACHESIZE 含义:

  CREATE DATABASE power CACHEMODEL both CACHESIZE 16;
  
  - CACHESIZE 单位:MB
  - 作用范围:每个 VNode 的缓存上限
  - 默认值:1 MB
  
  
  缓存大小估算:
  
    每个子表每列一个缓存条目
    条目大小 ≈ Key(16B) + Value(8B + 列宽) + RocksDB 开销
    
    例:10万子表 × 20列 × 2种(last_row+last_value) = 400万条目
    每条目 ≈ 50B → 总计 ≈ 200MB
    
    如果 CACHESIZE 设为 16MB → RocksDB 使用 16MB 内存
    → 超出部分自动落盘到 cache.rdb 文件
    → 查询仍可命中,但可能需要磁盘读取
    
    
  最佳实践:
    - CACHESIZE 不必覆盖所有缓存数据
    - RocksDB 自有 Block Cache 机制
    - 热点子表的缓存自然保持在内存中
    - 冷门子表的缓存存在磁盘上(仍比扫描 TSDB 快)

7. 缓存的持久化与恢复

复制代码
缓存的持久性:

  RocksDB cache.rdb 是持久化的:
  - VNode 重启后缓存仍然有效(无需重建)
  - 只有 DELETE/DROP 操作可能导致缓存失效需要重建
  
  缓存重建场景:
  1. 首次启用 CACHEMODEL → 后台扫描全部数据构建缓存
  2. VNode 迁移/恢复 → 从数据文件重建
  3. DELETE 操作后 → 局部重建受影响的条目
  
  重建开销:
  - 全量重建需要扫描所有 FileSet
  - 海量子表 + 大量文件时可能耗时较长
  - 重建期间查询可能退化为文件扫描

8. CACHEMODEL 变更的影响

sql 复制代码
-- 启用缓存(触发后台重建)
ALTER DATABASE power CACHEMODEL 'both';

-- 关闭缓存(释放内存和磁盘)
ALTER DATABASE power CACHEMODEL 'none';

-- 修改缓存大小
ALTER DATABASE power CACHESIZE 32;
复制代码
变更影响:

  none → both:
    触发全量缓存重建(后台执行)
    重建期间 LAST() 仍可执行(走文件扫描)
    重建完成后自动切换到缓存路径
    
  both → none:
    立即释放缓存内存
    删除 cache.rdb 文件
    后续 LAST() 查询走文件扫描路径

代码示例

配置与使用

sql 复制代码
-- 推荐配置:IoT 监控场景
CREATE DATABASE iot_monitor 
  CACHEMODEL 'both' 
  CACHESIZE 16;

-- 查询设备最新值(命中缓存,亚毫秒级)
SELECT LAST_ROW(ts, temperature, humidity) 
FROM iot_monitor.meters 
GROUP BY tbname;

-- 查询每列最后一个有效值
SELECT LAST(temperature), LAST(humidity) 
FROM iot_monitor.d1001;

验证缓存效果

sql 复制代码
-- 对比查询延迟
-- Step 1: CACHEMODEL='none' 下的查询时间
ALTER DATABASE power CACHEMODEL 'none';
SELECT LAST(temperature) FROM meters GROUP BY tbname;
-- 记录耗时

-- Step 2: CACHEMODEL='both' 下的查询时间
ALTER DATABASE power CACHEMODEL 'both';
-- 等待缓存重建完成
SELECT LAST(temperature) FROM meters GROUP BY tbname;
-- 对比耗时

性能考量

缓存的空间与时间权衡

子表数 CACHEMODEL 内存增加 LAST() 延迟
1 万 none 0 10~50ms
1 万 both ~10MB < 1ms
100 万 none 0 100~500ms
100 万 both ~500MB 1~5ms

RocksDB vs 内存缓存

特性 纯内存缓存 RocksDB 缓存
重启后 丢失 保留
容量限制 受内存限制 可落盘扩展
访问延迟 纳秒级 微秒级(内存命中)/ 毫秒级(磁盘)
适合场景 少量子表 海量子表

FAQ

Q1: CACHEMODEL='both' 内存会不会太大?

CACHESIZE 参数限制了 RocksDB 的内存用量。超出内存部分自动写入 cache.rdb 磁盘文件。即使 CACHESIZE 设置较小,常用子表的数据仍然能在内存中命中。

Q2: 乱序写入会影响缓存吗?

乱序数据(时间戳早于当前缓存的时间戳)不会更新缓存------缓存只保留"最新时间戳"的值。只有时间戳更新(大于缓存中的时间戳)的写入才会触发缓存更新。

Q3: DELETE 后 LAST() 返回错误值?

DELETE 会触发缓存失效和重建。在重建完成前的短暂窗口内,可能需要回退到文件扫描。系统保证最终一致性------重建完成后结果正确。

Q4: 只查 LAST_ROW 不查 LAST,应该选 last_row 还是 both?

last_row。这样只维护最后一行的缓存,内存消耗最小。both 模式需要为每列独立追踪最后一个非 NULL 值,更新和存储开销更大。

参考

系统构架篇

数据模型

存储引擎

关于 TDengine

TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。

相关推荐
0x3F(小茶)1 小时前
STM32 Bootloader与OTA升级
c语言·stm32·单片机·嵌入式硬件·物联网
段一凡-华北理工大学1 小时前
工业领域的Hadoop架构学习~系列文章13:数据湖架构 - 工业大数据的统一存储底座
大数据·人工智能·hadoop·分布式·架构·高炉炼铁·高炉智能化
真上帝的左手1 小时前
19. 大数据- BI 入门-数仓实战2-ODS 原始数据层
大数据·数据仓库·bi
半夜修仙1 小时前
RabbitMQ应用问题
数据库·分布式·缓存·rabbitmq
段一凡-华北理工大学1 小时前
工业领域的Hadoop架构学习~系列文章14:Hadoop集群部署 - 从规划到上线的全流程实践
大数据·数据库·人工智能·hadoop·学习·架构·高炉炼铁
J.Kuchiki1 小时前
【PostgreSQL内核学习:Unique 算子源码深度解读学习】
数据库·学习·postgresql
闹小艾1 小时前
旅游小程序制作开发教程:零基础轻松制作一个旅游小程序
大数据·小程序·旅游
Je1lyfish1 小时前
CMU15-445 (2025 Fall/2026 Spring) Project#4 - Concurrency Control
开发语言·数据库·c++·笔记·后端·算法·系统架构
我是一颗柠檬1 小时前
【Redis】Cluster集群Day11(2026年)
数据库·redis·后端·缓存