TDengine 读缓存(Read Cache)实战指南:用 CACHEMODEL / CACHESIZE 将 LAST / LAST_ROW 当前值查询提速数倍
2026/9/12 13:56:41 网站建设 项目流程

TDengine 读缓存(Read Cache)实战指南:用 CACHEMODEL / CACHESIZE 将 LAST / LAST_ROW 当前值查询提速数倍

【免费下载链接】TDengineHigh-performance, scalable time-series database designed for Industrial IoT (IIoT) scenarios项目地址: https://gitcode.com/GitHub_Trending/tde/TDengine

读缓存(Read Cache)是 TDengine 面向"当前值查询"场景的内置加速机制:把每个子表的最新数据常驻 vnode 内存,命中时LAST/LAST_ROW无需再扫描磁盘上的历史数据文件。本文以官方文档 Read Cache 为主体,结合数据库参数参考与源码实现,完整讲解CACHEMODEL/CACHESIZE的语义、配置方法、验证手段、性能对比实验,以及底层的规划器优化与 LRU 缓存原理。读完本文,你可以独立为任意业务库开启并调优读缓存,用一条 SQL 确认命中效果,并依据cacheload指标判断容量是否充足。

读缓存是什么:为"最新一条数据"而生的内存加速层

在工业物联网(IIoT)与设备监控场景中,最频繁的查询往往不是历史聚合,而是"这台设备现在的最新读数是多少"。TDengine 的读缓存把每个子表(subtable)最近写入的数据存放在 vnode 内存中,当查询命中缓存时,LAST/LAST_ROW不需要读取磁盘上的历史数据文件,从而显著降低响应延迟。典型使用场景包括:

  • 展示最新设备读数的实时仪表盘(Dashboard);
  • 每张表"最新状态"的轮询式刷新(如告警状态、在线状态);
  • 高并发下的LAST/LAST_ROW聚合查询。

需要区分的是,读缓存只是 TDengine 缓存体系中的一环。写缓存(BUFFER)、元数据缓存(PAGES/PAGESIZE)、WAL 相关的文件系统缓存(WAL_LEVEL/WAL_FSYNC_PERIOD)分别解决不同问题,完整机制参见 Data Caching 与 Architecture。

先分清两个函数:LAST 与 LAST_ROW

读缓存主要服务于两个聚合函数,它们的语义不同,因此需要匹配不同的CACHEMODEL取值:

函数语义(概述)完整参考
LAST某列最后写入的非 NULL 值(可以按列分别取值)LAST
LAST_ROW表 / 超表的最后一行(该行各列值可能为 NULL)LAST_ROW

两者都属于管道聚合函数(Pipeline Aggregate Function)。LAST返回的是"时间戳最大且非 NULL"的列值;LAST_ROW返回的是整条"时间戳最大的记录",这一行中的某些列完全可能是 NULL。选择CACHEMODEL时,应依据业务真正使用的函数:只查整行最新记录用last_row,按列取最新非 NULL 值用last_value,两者兼有则用both

配置读缓存:CACHEMODEL 与 CACHESIZE

读缓存由数据库级参数CACHEMODELCACHESIZE控制,可在CREATE DATABASE时设置,也可通过ALTER DATABASE动态调整。完整语法见 Databases(其中database_option包含CACHEMODEL {'none' | 'last_row' | 'last_value' | 'both'}CACHESIZE value)。

CACHEMODEL:四种缓存模式

取值含义主要加速对象
none不缓存(默认值)
last_row缓存每个子表最近的一行数据LAST_ROW
last_value缓存每列最近的非 NULL 值不受WHEREORDER BYGROUP BYINTERVAL等影响的LAST
both同时缓存最近一行与最近各列值LAST_ROW以及满足上述条件的LAST

需要注意的使用要点:

  • 频繁切换CACHEMODEL可能导致LAST/LAST_ROW结果短暂不准,请谨慎操作,建议开启后保持稳定。
  • 带过滤、排序、分组或时间窗口的LAST查询通常无法完全命中last_value缓存——这一点在规划器层面有严格检查(见下文源码分析)。
  • 开启读缓存后,写路径需要同步维护缓存(每次写入都要更新该子表的 last_row / last_value),可能影响写入性能。对于高吞吐写入场景,建议把both降为last_rowlast_value,详见 Ingesting Data Efficiently 中关于cachemodel影响写路径的说明。

CACHESIZE:每个 vnode 的缓存内存上限

CACHESIZE设置每个 vnode用于缓存子表最新数据的内存大小,默认值为1,取值范围[1, 65536],单位为 MB。应根据机器内存与表规模综合设置:表越多、单条记录越大,需要的容量越大。

如何判断容量是否足够(来自 Modify CACHESIZE 的操作方法):

  1. 查看配置值SELECT * FROM INFORMATION_SCHEMA.INS_DATABASES;可显示每个库配置的CACHESIZE(MB)。
  2. 查看实际占用SHOW db_name.VGROUPS;的输出包含cacheload列,表示当前 last-cache 的实际使用量(字节)。
  3. 判定原则:若cacheload非常接近cachesize,说明容量可能偏小,缓存频繁淘汰、命中率下降;若明显小于cachesize,则容量充足。调整幅度可结合系统可用内存决定,例如翻倍或数倍扩容。

进阶参数:CACHESHARDBITS(缓存分片)

在数据库参数体系中,还有一个与读缓存并发性能强相关的参数CACHESHARDBITS(见 Databases),它控制 last-value LRU 缓存的分片位数,即内部锁粒度。默认值-1(按 CACHESIZE 自动计算),取值范围[-1, 19]

  • 实际分片数 =2^CACHESHARDBITS。例如CACHESHARDBITS=3表示 8 个分片,CACHESHARDBITS=6表示 64 个分片。
  • 自动计算规则:每个分片至少 512 KB,理论最大分片数 =CACHESIZE / 512KB;分片位数 =floor(log₂(理论最大分片数)),上限为 6(最多 64 个分片);当CACHESIZE < 512 KB时位数为 0(单分片)。
CACHESIZE理论最大分片数分片位数实际分片数
1 MB212
4 MB838
32 MB64664
256 MB5126(封顶)64

更多分片可降低并发写缓存时的锁竞争,适合高并发场景;但分片过多会增加内存管理开销。注意:修改CACHESHARDBITS会立即令库内所有 vnode 的 last-value 缓存条目全部失效,后续查询需从磁盘重新加载,可能短暂抬升查询延迟——这与源码中"重建 last cache"的逻辑一致(见下文)。

创建与修改示例

-- 建库时开启 CREATE DATABASE power CACHEMODEL 'both' CACHESIZE 16; -- 对已有库开启或调整 ALTER DATABASE power CACHEMODEL 'both'; ALTER DATABASE power CACHESIZE 32;

开启后,使用SHOW CREATE DATABASE确认参数已生效,使用SHOW VGROUPS查看各 vnode 的cacheload(当前 last-cache 使用量,字节)。在 command.c 中可以看到,SHOW CREATE DATABASE的还原语句会把CACHESIZE %d CACHEMODEL '%s' CACHESHARDBITS %d一并输出,便于迁移时完整复刻配置。

实战演练:用 taosBenchmark 对比读缓存前后的查询延迟

官方文档给出了一套可复现的对比实验:使用智能电表数据,对比开启读缓存前后LAST/LAST_ROW的延迟。

第一步:生成测试数据

taosBenchmark -d power -Q --start-timestamp=1600000000000 --tables=10000 --records=10000 --time-step=10000 -y

该命令创建数据库power和超表meters,共约 1 亿行数据:10,000 个子表、每表 10,000 行,起始时间戳1600000000000(即2020-09-13T20:26:40+08:00),时间间隔 10 秒。此时CACHEMODEL为默认值none(不缓存)。

第二步:无缓存基线查询

taos> SELECT LAST(ts, current) FROM meters; last(ts) | last(current) | ================================================= 2020-09-15 00:13:10.000 | 1.1294620 | Query OK, 1 row(s) in set (0.353815s) taos> SELECT LAST_ROW(ts, current) FROM meters; last_row(ts) | last_row(current) | ================================================= 2020-09-15 00:13:10.000 | 1.1294620 | Query OK, 1 row(s) in set (0.344070s)

未开启缓存时,两条查询分别耗时约 353 ms 与 344 ms——每次都要扫描全表定位最新的行。

第三步:开启读缓存并确认生效

taos> ALTER DATABASE power CACHEMODEL 'both'; Query OK, 0 row(s) affected (0.046092s) taos> SHOW CREATE DATABASE power\G; *************************** 1.row *************************** Database: power Create Database: CREATE DATABASE `power` BUFFER 256 CACHESIZE 1 CACHEMODEL 'both' COMP 2 DURATION 14400m WAL_FSYNC_PERIOD 3000 MAXROWS 4096 MINROWS 100 STT_TRIGGER 2 KEEP 5256000m,5256000m,5256000m PAGES 256 PAGESIZE 4 PRECISION 'ms' REPLICA 1 WAL_LEVEL 1 VGROUPS 10 SINGLE_STABLE 0 TABLE_PREFIX 0 TABLE_SUFFIX 0 TSDB_PAGESIZE 4 WAL_RETENTION_PERIOD 3600 WAL_RETENTION_SIZE 0 KEEP_TIME_OFFSET 0 Query OK, 1 row(s) in set (0.000282s)

SHOW CREATE DATABASE输出中已出现CACHESIZE 1 CACHEMODEL 'both',说明配置生效。

第四步:再次查询对比

第一次查询会填充缓存,后续查询通常能观察到明显更低的延迟:

taos> SELECT LAST(ts, current) FROM meters; last(ts) | last(current) | ================================================= 2020-09-15 00:13:10.000 | 1.1294620 | Query OK, 1 row(s) in set (0.044021s) taos> SELECT LAST_ROW(ts, current) FROM meters; last_row(ts) | last_row(current) | ================================================= 2020-09-15 00:13:10.000 | 1.1294620 | Query OK, 1 row(s) in set (0.046682s)

本实验中,延迟从约 353 / 344 ms 降至约 44 ms,提速约 8 倍。实际效果取决于数据规模、硬件配置与并发负载,但方向是一致的:查询越频繁、数据越热,读缓存收益越明显

源码视角:读缓存何时真正生效

读缓存并非无条件加速所有LAST/LAST_ROW查询,规划器与存储层都有一整套判定与落盘逻辑。

规划器:只有在合适的查询形态下才走缓存扫描

在 planOptimizer.c 中,hasSuitableCache根据CACHEMODEL与查询中是否出现LAST_ROW/LAST决定能否应用lastRowScan优化:

  • TSDB_CACHE_MODEL_NONE:一律不优化;
  • TSDB_CACHE_MODEL_LAST_ROW:仅当查询含LAST_ROW时优化;
  • TSDB_CACHE_MODEL_LAST_VALUE:仅当查询含LAST时优化;
  • TSDB_CACHE_MODEL_BOTH:两者皆可。

同时,lastRowScanOptCheckFuncList(planOptimizer.c)还会检查函数列表中是否混入了其他函数、WHERE/ORDER BY/GROUP BY/INTERVAL等条件,一旦出现这类"特殊效果",查询就不会被优化为缓存扫描,而是回退到常规扫描路径——这正是文档中"带过滤、排序、分组或窗口的LAST查询往往无法完全使用last_value缓存"的源码依据。

存储层:LRU 缓存 + 惰性加载 + 写路径维护

在存储引擎 tsdbCache.c 中,last-cache 的载体是每个 vnode 的 LRU 缓存(pTsdb->lruCache),其行为与 Architecture: last/last_row Cache 的描述完全对应:

  • 惰性加载(lazy loading):对某张表的首次查询从内存池与磁盘读出所需值,写入 LRU 缓存后返回;后续插入或删除只更新已存在的缓存条目,未缓存的表写入不会强制进缓存。对子表查询只加载该子表,对超表查询可能加载其全部子表。
  • 状态标记:缓存条目带有ELastCacheStatus(tsdb.h),区分"缓存有效"(TSDB_LAST_CACHE_VALID)与"无缓存但 tsdb 可能有数据"(TSDB_LAST_CACHE_NO_CACHE)。
  • 容量控制tsdbCacheSetCapacity(tsdbCache.c)把CACHESIZE(MB)换算成字节设置 LRU 容量;tsdbCacheGetUsage返回当前使用量,正是SHOW VGROUPScacheload指标的数据来源。
  • 配置变更联动:在 vnodeSvr.c 中可以看到,CACHESIZE变化会立即调用tsdbCacheSetCapacity调整 LRU 容量;CACHESHARDBITS变化则加锁调用tsdbRebuildLastCache重建整个 last cache,并打印 "rebuilding last cache" 日志——印证了文档中"修改分片位会立即失效所有缓存条目"的警告。

写路径开销的量化指标

读缓存开启后,每次写入都需要更新对应子表的 last_row / last_value。vnode 的写入指标中维护了last_cache_commit_timelast_cache_commit_count(见 vnodeInt.h),在 vnodeCommit.c 中通过METRICS_TIMING_BLOCK统计每次缓存提交的耗时。这些指标可以直接印证"读缓存由写路径维护、影响写入性能"的结论——这也是官方建议高吞吐场景将both降级为last_rowlast_value的原因。

运维建议与最佳实践

  1. 按查询形态选模式:纯"最新状态"仪表盘用last_row;按列取最新非 NULL 值(如LAST(current))用last_value;两者都高频出现再用both,避免无谓的写路径开销。
  2. 开启后保持稳定:避免频繁在none/last_row/last_value/both之间切换,防止LAST/LAST_ROW结果短暂不准。
  3. 定期观察 cacheload:用SHOW VGROUPS对比cacheloadCACHESIZE,接近即扩容;扩容可结合CACHESHARDBITS的自动分片规则一起评估。
  4. 与写缓存等机制配合:读缓存只解决"最新值"查询;高频写入吞吐主要受BUFFERVGROUPSWAL_LEVEL/WAL_FSYNC_PERIOD影响,需要整体规划(参见 Data Caching)。
  5. 验证生效SHOW CREATE DATABASE确认参数、SHOW VGROUPS观察cacheload增长、以及查询延迟对比,三步即可完成上线验证。

相关文档

  • LAST / LAST_ROW:函数语义与使用约束
  • CACHEMODEL / CACHESIZE / CACHESHARDBITS:完整参数参考与容量判定方法
  • Data Caching:写缓存、元数据缓存、WAL 等其他缓存类型
  • Architecture: last/last_row Cache:LRU、惰性加载与配置联动机制
  • Ingesting Data Efficiently:读缓存对写路径的影响与高吞吐调优

【免费下载链接】TDengineHigh-performance, scalable time-series database designed for Industrial IoT (IIoT) scenarios项目地址: https://gitcode.com/GitHub_Trending/tde/TDengine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询