TiDB TTL 表设计与实现全解:行级到期数据自动清理的语法、调度架构与源码剖析
【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidb
TTL(Time To Live)表是 TiDB 提供的一种"数据自动过期清理"能力:为表指定一个时间列与保留时长后,后台任务会周期性扫描并删除已过期的行,广泛适用于验证码、会话、日志、事件流水等只需短期保留数据的场景。本文以 TTL 表设计提案 为骨架,结合当前仓库中pkg/ttl的实现源码,完整讲解 TTL 表的 DDL 语法、表级配置、后台作业调度(Job/Task)架构、扫描与删除执行细节、系统变量与监控指标,并给出可复现的 SQL 示例与源码级证据,帮助读者既能"会用",也能理解"如何实现"。
一、TTL 表要解决什么问题
在传统数据库中,清理"过期数据"通常依赖业务方定期执行DELETE,或由 DBA 编写定时脚本按时间字段分批删除。这类做法有两个痛点:删除语句容易与在线业务互相影响,且"多久删一次、删哪些行"完全游离于表结构之外,无法被数据库统一管理与运维。
TTL 表把"过期清理"变成一种表级声明式属性。一个 TTL 表包含一个类型为DATE/DATETIME/TIMESTAMP的时间列,该列与当前时间比较,当二者间隔超过设定的阈值(如 3 个月)时,对应行会被自动删除。如设计文档引言所述,其典型场景就是清理"过期的验证码"这类数据——写入后只需要存活有限时间,之后由数据库自动回收,业务代码无需再关心删除逻辑。
二、总体设计选型:为什么采用 SQL 层方案
设计文档明确了 TiDB TTL 的核心理念是SQL-layer approach:后台任务通过 SQL 协议扫描并删除过期行。相比把 TTL 下沉到存储层,这一方案的优势被明确列出:
- 实现简单,天然感知表结构与 SQL 语义;
- 与 TiCDC、BR(备份恢复)、二级索引等生态工具兼容良好,删除行为本身就是普通的 SQL DML,可被这些组件识别与同步。
设计文档同时分析了其他备选方案的短板:
| 候选方案 | 主要问题 |
|---|---|
| 复用 TiKV RawKV 上的 TTL | 不具备 SQL 感知;无法用任意列(含生成列)做 TTL 列;无法支撑未来外键;TTL 是 KV 级别的,不能按表开关;与 CDC、备份、二级索引不兼容 |
| 将 TTL 配置下推给 TiKV 执行 | 存在未解决的隔离性问题(如可能破坏快照隔离约束) |
| CockroachDB 行级 TTL | 理念相似,但 CockroachDB 用隐藏列存储"到期时间戳"而非直接比较"创建时间列",因此ALTERTTL 选项时会引发存量数据改写,性能受表大小影响 |
TiDB 选择"直接用创建时间列 + INTERVAL 表达式"的方式,ALTER表级 TTL 配置只改元数据、不触碰数据行,这是它在实现层面的一个优势。
三、TTL 表 DDL 语法完整指南
设计文档给出的语法分为建表、改表、移除三类,下面结合仓库实现逐一说明。
3.1 创建 TTL 表
最基本的 TTL 表把created_at声明为 TTL 时间列,数据在写入 3 个月后过期:
CREATE TABLE t1 ( id int PRIMARY KEY, created_at TIMESTAMP ) TTL = `created_at` + INTERVAL 3 MONTH;其中TTL = <time_column> + INTERVAL <n> <unit>即"到期时间 = 时间列值 + 保留时长"。保留时长由数值与时间单位组成,时间单位在语法层面支持到ast.TimeUnitType(如MONTH、DAY、HOUR等),可参考源码中对IntervalTimeUnit的建模(pkg/meta/model/table.go 中的TTLInfo结构体将IntervalExprStr与IntervalTimeUnit分开存储)。
3.2 用 TTL_ENABLE 开关后台任务
默认建表后 TTL 作业即为开启状态,若只想声明 TTL 语义而暂不执行删除,可显式关闭:
CREATE TABLE t1 ( id int PRIMARY KEY, created_at TIMESTAMP ) TTL = `created_at` + INTERVAL 3 MONTH TTL_ENABLE = 'OFF';TTL_ENABLE缺省时为ON。在 DDL 解析与落库阶段,getTTLInfoInOptions(见 pkg/ddl/ttl.go)会把TableOptionTTL、TableOptionTTLEnable、TableOptionTTLJobInterval三类选项聚合成model.TTLInfo:Enable默认置true,若同时出现TTL_ENABLE则以显式值为准(pkg/ddl/create_table.go、pkg/ddl/executor.go 中对ast.TableOptionTTL等选项的分支处理可印证)。
3.3 兼容 MySQL 的注释式语法
为使语法形态与 MySQL 生态兼容,TTL 选项同样支持放在特殊注释块中:
CREATE TABLE t1 ( id int PRIMARY KEY, created_at TIMESTAMP ) /*T![ttl] TTL = `created_at` + INTERVAL 3 MONTH TTL_ENABLE = 'OFF'*/;MySQL 及不支持 TTL 的下游工具会将其整体视为普通注释而忽略,TiDB 则解析其中的 TTL 配置。
3.4 ALTER 为已有表添加或更新 TTL
对一张已存在(非 TTL)表,可直接通过ALTER TABLE追加 TTL 属性:
ALTER TABLE t1 TTL = `created_at` + INTERVAL 3 MONTH;更新已有 TTL 配置时,ALTER会以最小覆盖原则合并:只修改本次给出的选项,其余选项沿用旧值。这一行为在 DDL 执行逻辑onTTLInfoChange中有明确实现——当只改TTL_ENABLE或只改TTL_JOB_INTERVAL而对应表尚未有 TTL 配置时,会报ErrSetTTLOptionForNonTTLTable(参见 pkg/ddl/ttl.go);更新完成后,该表正在运行的后台作业会按最新配置停止或重启。
文档示例中,TTL_JOB_INTERVAL用于自定义作业调度周期,例如把表t1的清理作业周期调整为 1 天:
ALTER TABLE t1 TTL_JOB_INTERVAL='1d';调度周期既支持设计文档里的ALTER表级设置,也支持在CREATE TABLE时一并声明。注意默认值的版本演进:设计文档写作时默认周期为 1 小时,而当前仓库中表级默认值已演进为24h(见 pkg/meta/model/table.go 中DefaultTTLJobInterval = "24h");同时保留了OldDefaultTTLJobInterval = "1h"作为 8.5 及之前版本的兼容值——当老版本建的表JobInterval字段为空时,GetJobInterval()会回退返回1h,保证从 v6.5 一路升级过来的集群行为一致。
3.5 移除 TTL:REMOVE TTL
与 MySQL 的ALTER TABLE ... REMOVE PARTITIONING思路类似,去掉表的 TTL 属性只需:
ALTER TABLE t1 REMOVE TTL;对应 DDL 内部流程为onTTLInfoRemove(pkg/ddl/ttl.go):置空tblInfo.TTLInfo并落库,作业随后被终止。
3.6 生成列作为 TTL 时间列
设计文档指出,TTL 表支持把生成列(Generated Column)作为时间列,例如把 JSON/VARCHAR 中的时间字段转换为DATETIME后再作为 TTL 列。但文档明确提示当前存在性能退化:
- 生成列的表达式条件无法下推到 TiKV,过滤工作只能在 TiDB 侧完成,扫描阶段网络与 CPU 开销更高;
- 多数日期函数同样不支持下推。
要从根本上解决,需要"生成列表达式支持下推 TiKV"与"常用日期函数支持下推 TiKV"两项能力(详见本文"已知问题"一节)。
3.7 使用约束与合法性校验
TTL 表并非可以随意声明,仓库 pkg/ddl/ttl.go 的checkTTLInfoValid汇总了全部校验规则,可归纳为:
- 外键约束:被其他表通过外键引用的表不能加 TTL(父表被引用时,删除父表行可能违反子表外键约束),对应报错
ErrUnsupportedTTLReferencedByFK。设计文档给出的描述与该实现一致。 - 时间列类型:TTL 时间列必须是时间类型(
DATE/DATETIME/TIMESTAMP),否则报ErrUnsupportedColumnInTTLConfig。 - 列存在性:TTL 配置指向的列必须真实存在,否则报
ErrBadField。 - 临时表:临时表不允许设置 TTL(
ErrTempTableNotAllowedWithTTL)。 - 聚簇主键类型:若表使用聚簇主键且主键含
FLOAT/DOUBLE列,不允许创建 TTL(ErrUnsupportedPrimaryKeyTypeWithTTL)。原因是删除过期行走的是 SQLDELETE ... WHERE PK IN (...),浮点主键在比较时存在精度损失风险。 - 间隔表达式合法性:创建/修改时会用
cache.EvalExpireTime预计算,校验INTERVAL表达式可被正确求值。 - 列保护:TTL 时间列不能被
DROP COLUMN删除(ErrTTLColumnCannotDrop)。
四、表级 TTL 配置在元数据中的存储形态
TTL 属性最终落在每张表的元数据TableInfo.TTLInfo上,其结构定义在 pkg/meta/model/table.go:
| 字段 | 含义 |
|---|---|
ColumnName | TTL 时间列名 |
IntervalExprStr | 间隔表达式的文本(如INTERVAL 3 MONTH中3 MONTH部分经过 RESTORE 后的字符串) |
IntervalTimeUnit | 间隔时间单位(内部为ast.TimeUnitType,以int存储避免循环依赖) |
Enable | 该表 TTL 作业是否启用(对应TTL_ENABLE) |
JobInterval | 两次 TTL 作业之间的间隔(字符串形式,如24h) |
JobInterval的解析统一走(*TTLInfo).GetJobInterval(),其中注入了 failpointoverwrite-ttl-job-interval以便测试覆盖。这也说明"多久跑一次作业"在最终实现中是每表各自可配置的(设计文档中提及的全局tidb_ttl_job_run_interval变量,在落地演进中被表级TTL_JOB_INTERVAL+ 全局作业开关等变量取代,使用时请以当前版本的 系统变量清单 为准)。
五、后台作业调度与任务管理架构
5.1 作业模型概述
设计文档给出的作业模型是:
- 为每个 TTL 表在"需要时"调度一个作业(Job),作业与物理表一一对应;
- 尽量把不同表的作业分散到不同 TiDB 节点执行,降低对单个节点的影响;
- 一个分区表会被视为多个物理表:每个分区对应一条状态记录,因而同一时刻可能有多个作业在不同 TiDB 节点上并行运行。
在 pkg/ttl/ttlworker/config.go 中可以看到调度器的若干关键周期常量,例如作业管理器主循环 10s、任务管理器循环 1 分钟、任务心跳 1 分钟、任务领取轮询 5 秒、作业超时上限 6 小时等;这些常量均可通过 failpoint 注入改写以便集成测试,从侧面印证调度循环的周期性特征。
5.2 状态记录表 mysql.tidb_ttl_table_status
每个 TTL 物理表的状态记录在系统表mysql.tidb_ttl_table_status中。设计文档给出的建表结构完整继承了表中全部字段:
CREATE TABLE `tidb_ttl_table_status` ( `table_id` bigint(64) PRIMARY KEY, `parent_table_id` bigint(64), `table_statistics` TEXT DEFAULT NULL, `last_job_id` varchar(64) DEFAULT NULL, `last_job_start_time` timestamp NULL DEFAULT NULL, `last_job_finish_time` timestamp NULL DEFAULT NULL, `last_job_ttl_expire` timestamp NULL DEFAULT NULL, `last_job_summary` text DEFAULT NULL, `current_job_id` varchar(64) DEFAULT NULL, `current_job_owner_id` varchar(64) DEFAULT NULL, `current_job_owner_addr` varchar(256) DEFAULT NULL, `current_job_owner_hb_time` timestamp, `current_job_start_time` timestamp NULL DEFAULT NULL, `current_job_ttl_expire` timestamp NULL DEFAULT NULL, `current_job_state` text DEFAULT NULL, `current_job_status` varchar(64) DEFAULT NULL, `current_job_status_update_time` timestamp NULL DEFAULT NULL );该表在集群 bootstrap 阶段随其他系统表一同创建,见 pkg/session/bootstrap.go(tidb_ttl_table_status与后文tidb_ttl_task均在其中注册)。字段语义按设计文档可归纳为:
table_id:TTL 表(物理表)ID;分区表场景下是每个分区的物理 ID。parent_table_id:若当前行为某个分区,则为其父表 ID,否则等于table_id。table_statistics:表的统计信息。last_job_*前缀:最近一次成功执行作业的信息,包括作业 ID、开始/完成时间、本次作业使用的过期时间(last_job_ttl_expire)以及作业摘要(last_job_summary)。current_job_*前缀:当前尚未结束作业的信息,除 ID、开始时间、过期时间外,还包括:current_job_owner_id/current_job_owner_addr/current_job_owner_hb_time:作业宿主(某个 TiDB 节点)的 ID、地址与心跳时间。宿主周期性刷新心跳;若心跳长期不更新,说明原宿主已下线,作业会被其他节点接管(fail over)。current_job_state:作业内部状态,用于失败后的状态恢复。current_job_status:作业生命周期状态。current_job_status_update_time:状态最近更新时间。
设计文档给出的current_job_status枚举为waiting / running / cancelling / cancelled / error;在 pkg/ttl/cache/ttlstatus.go 的实现中,该枚举进一步演进为waiting / running / cancelling / cancelled / timeout / finished六态,实际使用时以上述源码定义为准。current_job_id不只代表正在运行的作业,失败或用户取消但尚未清理的作业同样保留其中,方便排查。
5.3 取消运行中的作业
需要终止某个作业时,执行管理命令:
ADMIN CANCEL TTL JOB 123456789作业状态会先变为cancelling,随后最终更新为cancelled。
5.4 分布式任务表 mysql.tidb_ttl_task
为了让集群资源得到最大化利用,设计文档提出把单个作业拆成多个扫描任务并分发到所有 TiDB 节点执行:
- 每个新作业创建时,向系统表
mysql.tidb_ttl_task写入若干行,每行代表一个扫描任务; - 每个 TiDB 节点周期性扫描该表,发现新任务后把任务 owner 置为自己并执行。
在 pkg/ttl/ttlworker/task_manager.go 中可以看到对应的 SQL 模板族:UPDATE mysql.tidb_ttl_task设置任务 owner、标记任务完成、刷新心跳、放弃 owner(resign),以及SELECT count(1) ... WHERE status = 'running'统计运行中任务数,完整佐证了"认领—执行—心跳—收尾"的分布式任务生命周期。任务行还记录了扫描范围(scan_range_start/scan_range_end)、过期时间与创建时间,见 pkg/ttl/cache/task.go。集成测试 pkg/ttl/ttlworker/task_manager_integration_test.go 中模拟多 TiDB 节点(task-manager-1/task-manager-2)竞争领取任务并验证"running → finished"状态迁移,是理解该机制最直接的样例。
六、作业执行细节:扫描与删除
一个运行中的 TTL 作业包含两类任务:扫描任务(Scan Tasks)负责从表中过滤出过期行,删除任务(Delete Tasks)负责批量删除。全部过期行删完后作业结束。单个 TiDB 节点上运行多个 worker 并发消费这些任务。
6.1 扫描任务:按主键范围分批翻页
作业开始时,先把表按主键切分成 N(N ≥ 1)个范围,每个范围交给一个扫描任务执行范围扫描。设计文档给出的伪代码如下:
func doScanTask(tbl, range, expire, ch) { var lastRow for { selectSQL := buildSelect(tbl, range lastRow, expire, LIMIT) rows := execute(selectSQL) ch <- deleteTask{tbl, expire, rows} if len(rows) < LIMIT { break } lastRow := rows[len(rows)-1] } }首次查询形如:
SELECT LOW_PRIORITY id FROM t1 WHERE create_time < '2022-01-01 00:00:00' AND id >= 12345 AND id < 45678 ORDER BY id ASC LIMIT 500;其中'2022-01-01 00:00:00'是作业启动时刻计算出的过期时间,[12345, 45678)是当前扫描任务负责的键范围,LIMIT 500为单次最多返回行数,可用系统变量tidb_ttl_scan_batch_size调整。由于一次查询通常取不完所有过期行,当返回行数达到上限时,就把上次读到的最后一行主键(如23456)作为下一次查询的起点继续翻页:
SELECT LOW_PRIORITY id FROM t1 WHERE create_time < '2022-01-01 00:00:00' AND id >= 23456 AND id < 45678 ORDER BY id ASC LIMIT 500;直到某次返回行数不足LIMIT即代表该范围读尽。需要注意的关键点:
- 扫描很重:它本质上是全表范围扫描,因此对大表而言,建议把作业周期(如
TTL_JOB_INTERVAL/ 作业调度窗口)调大,摊薄单日资源开销。 - 无主键/非聚簇主键表:使用隐藏列
_tidb_rowid作为行号参与范围切分与翻页。 - 生成列时间列:条件无法下推时,过滤在 TiDB 侧完成,需要更多网络流量与 CPU。
在实现层面,pkg/ttl/ttlworker/scan.go 的doScanWithSession除了逐页翻页(读取当前vardef.TTLScanBatchSize作为limit),还做了两类加固,比设计文档更进一步:
- 安全过期时间校验:任务执行前会把"作业计算出的过期时间"与"按当前最新配置重新计算的安全过期时间"对比。因为作业提交后、任务真正执行前,用户可能已把保留时长从 2 天改成 1 天,若仍按旧的过期时间删除就可能误删本不该删的行,因此校验不通过时任务会直接报错中止(对应代码注释中的完整时序说明)。
- 失败重试:单条扫描 SQL 执行失败时按
scanTaskExecuteSQLMaxRetry = 5次、间隔 2 秒的重试策略处理(pkg/ttl/ttlworker/config.go 与 pkg/ttl/ttlworker/scan.go)。
6.2 删除任务:分批多行 DELETE
删除 worker 从 channel 中消费扫描阶段投递的任务,再把行拆成若干批次。设计文档伪代码如下:
func doDelTask(ch) { for _, task := range ch { batches := splitRowsToDeleteBatches(task.rows) for _, batch := range batches { deleteBatch(task.tbl, task.batch, task.expire) } } }批次大小由系统变量tidb_ttl_delete_batch_size控制,每个批次用一条多行DELETE完成:
DELETE LOW_PRIORITY FROM t WHERE id in (1, 2, 3, ...) AND create_time < '2022-01-01 00:00:00';这里刻意保留create_time < '...'条件,是为了避免误删:某行在扫描时已过期,但在真正执行删除前可能被业务更新为未过期值,此时id IN (...)即使命中,也会因时间条件不满足而被跳过。
关于性能,设计文档指出:若 TTL 表没有二级索引,传入删除的行大多落在同一 Region,删除操作多数情况下可以用1PC(一阶段提交)提交,事务开销显著降低。因此对大表而言,不建二级索引往往反而能获得更好的 TTL 清理性能。
6.3 SQL 构造器的实现佐证
扫描与删除 SQL 并非手工字符串拼接,而是由 pkg/ttl/sqlbuilder/sql.go 的SQLBuilder状态机式生成。可以看到:
- 查询以
SELECT LOW_PRIORITY SQL_NO_CACHE开头,删除以DELETE LOW_PRIORITY FROM开头——LOW_PRIORITY用于降低 TTL 后台任务对在线查询的影响,与设计文档示例一致; - 建表 SQL 支持分区表,构造时会追加
PARTITION(<name>)限定到具体分区; - 构造器内部强制要求写入
time_col < expire的过期条件(WriteCommonCondition),否则在生成最终 SQL 时报"expire condition not write"错误——从代码层面保证"任何删除都携带过期条件",杜绝无差别删除。
七、时区考量
TTL 时间列支持三种字段类型:Date、DateTime与TimeStamp。其中Date/DateTime不是绝对时间点,必须结合时区才能确定精确的到期时刻,设计文档列出了两种时区选择方案及其隐患:
- 使用系统时区(全局变量
system_time_zone,集群 bootstrap 时确定且之后不变):行会在固定的绝对时刻被删除,不会因用户改时区而漂移;但如果集群把会话/全局time_zone设置为与SYSTEM不同(例如system_time_zone为+08:00而time_zone为UTC),在作业周期小于 8 小时时,记录会被"立即视为过期"而误删。 - 使用集群时区(变量
time_zone):在time_zone不变化时表现良好;但一旦用户修改time_zone,TTL 作业必须及时感知新时区,否则"某行是否过期"在不知道历史时区的情况下无法判定,可能删除非预期行。
这一节说明:TTL 表在使用DATE/DATETIME作为时间列时,部署时应保证集群时区设置长期稳定,并清楚其相对时间的语义;TIMESTAMP存储的是绝对时间点,不存在此类歧义。
八、系统变量全览
设计文档规划了 8 个全局系统变量。对照仓库中的变量注册表 pkg/sessionctx/vardef/tidb_vars.go 与 pkg/sessionctx/variable/sysvar.go,实际落地的变量清单与参数如下(个别默认值在实现中已与设计稿不同,已按仓库当前定义修正):
| 系统变量 | 作用 | 作用域 | 取值范围 | 默认值 |
|---|---|---|---|---|
tidb_ttl_job_enable | 全局总开关;OFF时停止调度新作业并取消正在运行的作业 | Global | ON/OFF | ON |
tidb_ttl_job_schedule_window_start_time | TTL 作业调度时间窗口的开始时刻 | Global | 时间值 | 00:00 +0000 |
tidb_ttl_job_schedule_window_end_time | TTL 作业调度时间窗口的结束时刻 | Global | 时间值 | 23:59 +0000 |
tidb_ttl_scan_worker_count | 每个 TiDB 节点的扫描 worker 数 | Global | [1, 256] | 4 |
tidb_ttl_scan_batch_size | 扫描任务中每条 SELECT 的 LIMIT 值 | Global | [1, 10240] | 500 |
tidb_ttl_delete_worker_count | 每个 TiDB 节点的删除 worker 数 | Global | [1, 256] | 4 |
tidb_ttl_delete_batch_size | 每条 DELETE 的批次大小 | Global | [1, 10240] | 100 |
tidb_ttl_delete_rate_limit | 每个 TiDB 节点的删除速率上限,0 表示不限速 | Global | [0, MaxInt64] | 0 |
tidb_ttl_running_tasks | 集群中允许同时运行的任务数上限;支持自动值 | Global | 自动值或 [1, 最大并发] | -1(由 TiDB 自动决定,早期设计稿为 0) |
以上变量的当前默认值可以从vardef包中的DefTiDBTTL*常量直接核对(如DefTiDBTTLScanBatchSize = 500、DefTiDBTTLDeleteBatchSize = 100、DefTiDBTTLScanWorkerCount = 4等)。其中tidb_ttl_running_tasks在设计稿中的含义是"最大并行任务数,-1表示由 TiDB 自动决定";变量类型注册为TypeInt、AllowAutoValue: true,说明允许赋自动值以让系统按集群规模动态确定。
使用方式示例:
SET @@global.tidb_ttl_job_enable = 'ON'; SET @@global.tidb_ttl_scan_worker_count = 8; SET @@global.tidb_ttl_delete_batch_size = 200; SET @@global.tidb_ttl_job_schedule_window_start_time = '01:00 +0000'; SET @@global.tidb_ttl_job_schedule_window_end_time = '05:00 +0000';九、可观测性:TTL 监控指标
设计文档规划了 6 组 Prometheus 指标,用于观察 TTL 作业健康度与开销;相关定义可在 pkg/ttl/metrics/metrics.go 及全局 metrics 定义中找到对应实现:
| 指标 | 类型 | Labels | 含义 |
|---|---|---|---|
ttl_queries | Counter | sql_type,result | TTL 作业执行的查询总数 |
ttl_processed_expired_rows | Counter | sql_type | TTL 作业处理的过期行总数 |
ttl_query_duration | Histogram | sql_type,result | TTL 查询耗时分布 |
ttl_job_status | Gauge | (实现中按作业状态打标) | 当前 TTL 作业所处状态;处于该状态时为 1,否则为 0 |
ttl_phase_time | Counter | type,phase | 各 worker 在不同执行阶段(如 idle、begin/commit txn、query、wait retry、dispatch、check TTL、wait token 等,见 pkg/ttl/metrics/metrics.go 中的 Phase 常量)的耗时 |
ttl_insert_rows | Counter | — | 写入 TTL 表的总行数 |
设计文档对ttl_queries等指标的sql_type取值为select/delete,result取值为ok/error。在实现中扫描、删除成功与失败的行数分别通过不同 Counter 记录(如ScannedExpiredRows、DeleteSuccessExpiredRows、DeleteErrorExpiredRows),运行中/取消中的作业数量由RunningJobsCnt、CancellingJobsCnt等 Gauge 体现。这些指标配合 Grafana 即可监控"TTL 是否在按窗口执行、单位时间清理了多少行、后台任务对集群压力有多大"。
十、已知问题与性能影响
设计文档明确列出的已知问题是:
生成列条件无法下推到 TiKV。若表使用生成列作为 TTL 时间列,过滤会在 TiDB 侧完成,带来额外网络流量并使查询变慢。
该限制在文档写作时存在,也属于"未来工作"中性能优化部分的头号议题(下推生成列表达式、或直接用生成列定义构造过滤条件)。因此当前使用 TTL 时,若对清理性能敏感,应优先选择真实时间列而非生成列。另外如前文所述,无二级索引的表由于删除通常可走 1PC、且无索引写入热点,往往表现更好。
十一、未来演进方向
11.1 性能优化
设计文档规划的优化方向包括:
- 若 TTL 表存在 TiFlash 副本,扫描改走 TiFlash 以卸载 TiKV 压力;
- 若存在以 TTL 时间列为前缀的索引,用索引定点查询过期行,替代全表范围扫描,缩短扫描耗时;
- 对无二级索引的表,利用统计信息缓存(例如作业结束后缓存每个 Region 最旧行的创建时间),下次作业开始时跳过"无更新且未过期"的 Region;
- 对无二级索引的表,把扫描与删除下推到 TiKV 侧执行,避免 TiDB 与 TiKV 之间的数据搬运——类似 GCWorker,引入新的 Coprocessor 命令
TTLGC,由 TiKV 以非事务方式直接清理过期行; - 结合全局资源控制框架(设计时仍在进行中),最大化集群资源利用率。
11.2 更多功能支持
- 支持 TTL 表作为被
ON DELETE CASCADE子表引用的父表,实现级联清理; - 支持将生成列条件下推 TiKV,或用生成列定义直接构造过滤条件;
- 根据集群当前负载动态调整运行期参数;
- 为云环境增加内置 ALTER 支持。
十二、测试方案与仓库验证
设计文档给出了功能与性能两方面的测试计划:
功能测试(Functional Test):
- DDL 操作:创建带 TTL 的表、对非 TTL 表执行 ALTER 增加 TTL、对 TTL 表 ALTER 更新 TTL 选项、移除表的 TTL 选项;
- TTL 表能调度后台作业并删除过期行;
TTL_ENABLE或全局@@global.tidb_ttl_job_enable为OFF时表不调度作业;- 作业只在配置的时间窗口内被调度。
性能测试(Performance Test):
- 构造 1000 万行表、其中 10% 为过期行,测试清空过期数据所需时间;
- 同时运行基准测试与 TTL 作业,采集基准的 QPS/延迟并与未运行 TTL 时对比,评估对在线负载的影响。
这些计划在当前仓库中均有对应的工程化验证:DDL 侧有 pkg/ddl/ttl_test.go(校验默认间隔、TTL_ENABLE选项的解析与持久化)、pkg/ddl/db_table_test.go;作业与任务侧有 pkg/ttl/ttlworker/job_manager_test.go、pkg/ttl/ttlworker/scan_test.go、pkg/ttl/ttlworker/del_test.go,以及多节点场景的 pkg/ttl/ttlworker/integrationtest 集成测试目录;TTL 表状态读写与字段抽取逻辑见 pkg/ttl/cache/ttlstatus_test.go。读者若想深入理解某个机制,直接阅读对应测试是最高效的入口。
结语
TiDB TTL 表把"过期数据自动清理"做成了表级声明式能力:通过TTL = 时间列 + INTERVAL语法定义保留策略,由 TiDB 后台以 SQL 层作业(Job 拆分为 Scan/Delete 任务)在集群内分布执行。相比手工脚本,它具备统一元数据管理、天然兼容 TiCDC/BR/二级索引、作业状态可观测、可全局开关与限速等工程化优势;相比存储层 TTL 方案,则避免了 KV 级语义带来的隔离性、表感知与外键兼容问题。理解其语法、调度状态机、任务分发与各项系统变量,是在生产环境中正确、高效使用 TTL 的关键前提。
【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidb
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考