TiDB Timer Framework 使用指南:基于持久化存储的分布式定时任务调度框架
【免费下载链接】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
导读
TiDB 的pkg/timer是一个内部定时任务调度框架,专门解决"在分布式环境下周期性地触发某个动作、且需要持久化调度状态与消息投递语义"这类需求。它把"何时触发"(调度策略)与"触发后干什么"(Hook)彻底解耦,调度状态全部落盘(生产环境为 TiDB 系统表),因此天然支持进程重启恢复、多节点只触发一次、按 key 语义查询与幂等创建等能力。读完本文你将掌握:如何用 Timer Client 创建/查询/更新/删除定时器,如何搭建 TimerRuntime 驱动调度循环,以及如何实现自定义 Hook 类并正确处理延迟、消息语义与故障恢复。
一、框架定位:它解决什么问题
Timer framework 是 TiDB 内部用于周期性执行任务的框架。当你遇到以下场景时,可以优先考虑它(依据 pkg/timer/README.md):
- 按特定调度语义周期执行:例如每 5 分钟执行一次,或每天 00:00:00 执行一次;
- 分布式环境下的唯一执行:例如集群有 3 个 TiDB 节点,只希望任务在其中一个节点上执行,并且 TiDB 进程重启后要做恢复工作;
- 需要读取持久化的调度运行时信息:例如把定时器的状态暴露给监控系统使用;
- 触发动作需要满足消息投递语义:如 at-least-once(至少一次)、at-most-once(至多一次)、exactly-once(恰好一次)。
不适合的场景
框架明确声明两类场景不适用:
- 频率过高的任务:例如每 1 秒执行一次。虽然可以设置
1s的调度间隔,但每次触发框架都会对 store 做若干次调用,效率很低。这种场景应直接在代码里使用 Go 自带的time.Timer。 - 跨节点重型作业调度:框架只帮你调度"触发"(trigger)这个动作,触发动作本身应当轻量、能在短时间内完成。如果需要调度重型任务,应使用
pkg/disttask之类的分布式任务调度器(pkg/timer/README.md 中明确给出了该建议)。
一句话总结定位:Timer framework = 持久化的、分布式的、可恢复的"触发器",触发后的业务动作由你自己实现的 Hook 完成。
二、整体架构与核心概念
从源码结构看,pkg/timer分为四个子包,职责清晰(pkg/timer):
| 子包 | 职责 |
|---|---|
api | 核心数据模型与接口:TimerSpec、TimerRecord、TimerClient、TimerStore、Hook、调度策略等 |
runtime | 调度运行时:TimerGroupRuntime、hook worker、缓存与 watch 机制 |
tablestore | 基于 TiDB 系统表的持久化 store 实现 |
metrics | Prometheus 监控指标 |
关键对象
TimerSpec(pkg/timer/api/timer.go#L135-L159)是创建定时器时的规格描述,包含:
Namespace/Key:命名空间与键,Key在同一 namespace 内唯一;Tags:标签,可用于查询过滤;Data:用户自定义二进制数据,触发时透传给 Hook;TimeZone:评估调度策略使用的时区(为空则用 TiDB 集群时区);SchedPolicyType/SchedPolicyExpr:调度策略类型与表达式;HookClass:Hook 类名,决定由哪个 Hook 处理触发;Watermark:调度进度水位线;Enable:是否启用。
TimerRecord(pkg/timer/api/timer.go#L219-L248)是定时器在 store 中的完整记录,在TimerSpec之上增加了框架管理的运行时字段:
ID:store 自动分配的唯一标识;EventStatus:当前事件状态,IDLE或TRIGGER;EventID/EventData/EventStart:当前触发事件的 ID、数据与开始时间;Version:版本号,每次更新递增,用于乐观并发控制;CreateTime、Location等。
TimerStore(pkg/timer/api/store.go#L389-L437)是持久化抽象,核心方法包括Create、List、Update、Delete、Watch。框架通过WatchSupported()判断 store 是否支持变更推送。
两种调度策略
SchedPolicyType支持两种枚举值(pkg/timer/api/timer.go#L30-L35):
| 类型 | 值 | 语义 | 表达式示例 |
|---|---|---|---|
| INTERVAL | SchedEventInterval | 固定间隔触发 | 1h、30m、5s |
| CRON | SchedEventCron | cron 表达式触发 | 0 0 0 * * *(每天 00:00:00) |
对应的策略实现为SchedIntervalPolicy(pkg/timer/api/timer.go#L45-L73)与CronPolicy(pkg/timer/api/timer.go#L76-L96)。interval 表达式复用 TiDB parser 的duration.ParseDuration解析;cron 表达式使用github.com/robfig/cron/v3的cron.ParseStandard解析(即标准 5 段 + 可选秒段格式)。二者都实现统一的SchedEventPolicy.NextEventTime(watermark)接口,runtime 通过该接口计算"基于 watermark 的下一次触发时间"。
三、Timer Client 操作:定时器的增删改查
Client 是面向使用者的入口,任何定时器操作都必须从 client 开始。完整接口定义见 pkg/timer/api/client.go#L115-L135。
3.1 创建 Timer Client
client 必须从 store 创建。生产环境使用 TiDB 系统表(tablestore.NewTableTimerStore),本地测试可以使用内存 store:
package main import ( "fmt" "github.com/pingcap/tidb/timer/api" ) func main() { store := api.NewMemoryTimerStore() client := api.NewDefaultTimerClient(store) fmt.Println(client.GetDefaultNamespace()) }client.GetDefaultNamespace()返回当前 client 的 namespace,用于未来区分不同租户,目前恒为default(对应源码常量 DefaultStoreNamespace)。
内存 store 实现位于 pkg/timer/api/mem_store.go,通过uuid.New()生成 16 字节 ID 并做 hex 编码。注意:ID 不允许手动指定,Create时会校验record.ID、record.Version、record.CreateTime必须为零值(pkg/timer/api/mem_store.go#L52-L71),创建时EventStatus自动初始化为IDLE。
要查看如何从 TiDB 系统表创建 store,可阅读tablestore.NewTableTimerStore(pkg/timer/tablestore/store.go#L47-L63)。它接收 clusterID、session 池、库名、表名和 etcd client;传入 etcd 时使用 etcd 实现变更通知(NewEtcdNotifier),否则退化为内存通知器。
3.2 创建 Timer
想要每小时执行一次任务,可以这样创建:
timer, err := client.CreateTimer(ctx, api.TimerSpec{ Key: "/your/timer/key", SchedPolicyType: api.SchedEventInterval, SchedPolicyExpr: "1h", HookClass: "your.hook.class.name", Watermark: time.Now(), Data: []byte("yourdata"), Enable: true, }) if err != nil { // handle err } fmt.Printf("created timer id: %s\n", timer.ID)各字段说明:
Key:同一 namespace 下唯一。想用相同 key 重建,必须先删除旧的。SchedPolicyType/SchedPolicyExpr:INTERVAL+1h表示每 1 小时触发;CRON+0 0 0 * * *表示每天 00:00:00 触发。CreateTimer内部会调用TimerSpec.Validate()(pkg/timer/api/timer.go#L167-L190)校验 namespace、key、时区与调度策略表达式合法性,非法表达式会直接报错。HookClass:可选。指定后框架会调用对应的自定义 Hook;不指定时,框架只把事件状态置为TRIGGER,不会调用任何自定义 Hook 代码。Watermark:可选。设置了它,定时器只会在 watermark 之后存在未触发事件时才触发;不设置则创建后立即尝试触发一次。Enable:可选但关键。要保持定时器持续调度新事件,必须设为true;为false时停止调度新事件,直到重新置为true。这一点在TimerRecord.NextEventTime()中体现——Enable为 false 时直接返回"无下一个事件"(pkg/timer/api/timer.go#L250-L268)。Data:可选二进制数据,触发时框架会传给 Hook。ID:创建成功后由框架自动分配(memory store 为 UUID hex,table store 为last_insert_id,见 pkg/timer/tablestore/store.go#L113-L118),后续所有操作都通过 ID 进行。
3.3 查询 Timer
Client 提供三种查询方式(对应 pkg/timer/api/client.go 的GetTimerByID/GetTimerByKey/GetTimers):
// 按 key 前缀查询一批 timer timers, err := client.GetTimers(context.TODO(), api.WithKeyPrefix("/your/timer/")) if err != nil { // handle err ... } // 按 key 精确查询(限定在当前 namespace) timer, err := client.GetTimerByKey(ctx, "/your/timer/key") if err != nil { // handle err ... } // 按 id 查询 timer, err := client.GetTimerByID(ctx, "123") if err != nil { // handle err ... }GetTimers支持多种过滤选项:WithKey、WithKeyPrefix、WithID、WithTag(pkg/timer/api/client.go#L36-L67)。这些选项最终组合成一个TimerCond条件对象,底层过滤逻辑由TimerCond.Match实现(pkg/timer/api/store.go#L109-L138),支持 ID、namespace、key(前缀或精确)、tags 的组合匹配。除此之外,条件体系还提供api.And、api.Or、api.Not组合算子(pkg/timer/api/store.go#L300-L358),可以在构建 runtime 时表达更复杂的过滤逻辑。
3.4 更新 Timer
用UpdateTimer更新元数据,更新通过函数式选项(Option)描述,避免全量覆盖:
// 更新调度策略为 cron:每小时整点触发 if err := client.UpdateTimer(ctx, timer.ID, api.WithSetSchedExpr(api.SchedEventCron, "0 0 * * * *")); err != nil { // handle error } // 更新 watermark if err := client.UpdateTimer(ctx, timer.ID, api.WithSetWatermark(time.Now())); err != nil { // handle error }可选更新选项包括(pkg/timer/api/client.go#L72-L113):
WithSetEnable(enable):开关定时器;WithSetTimeZone(name):修改时区;WithSetSchedExpr(tp, expr):修改调度策略;WithSetWatermark(t):重置水位线;WithSetSummaryData(summary):更新汇总数据;WithSetTags(tags):更新标签。
更新底层是TimerUpdate结构体(pkg/timer/api/store.go#L160-L262),所有字段都是OptionalVal[T]包装——只有显式 Set 过的字段才会被应用,未设置字段保持不变,从而避免并发更新互相覆盖。更新还支持CheckVersion/CheckEventID条件校验(乐观并发控制,版本不匹配时返回ErrVersionNotMatch)。
3.5 删除 Timer
exist, err := client.DeleteTimer(ctx, timer.ID) if err != nil { // handle error }DeleteTimer返回额外布尔值exist:timer 存在则删除并返回true,不存在返回false(memory store 实现见 pkg/timer/api/mem_store.go#L154-L170)。
四、搭建 Runtime:让定时器真正跑起来
只创建 timer 不会让它运行。你必须搭建一个 timer runtime 来调度定时器,核心类型是runtime.TimerGroupRuntime。
4.1 创建并启动 Runtime
package main import "github.com/pingcap/tidb/timer/api" func main() { ctx := ... // some go context store := ... // Create a timer store. rt := runtime.NewTimerRuntimeBuilder("myGroup", store). RegisterHookFactory("your.hook.class.name", MyTimerHookFactory). SetCond(&api.TimerCond{ Key: api.NewOptionalVal("/"), KeyPrefix: true, }). Build() rt.Start() defer rt.Stop() <-ctx.Done() }Builder 方法说明(源码见 pkg/timer/runtime/runtime.go#L85-L100):
NewTimerRuntimeBuilder(groupID, store):group 名称目前只用于监控指标前缀(如runtime.<groupID>.*)。RegisterHookFactory(hookClass, factory):注册自定义 Hook 工厂。runtime 遇到某个HookClass的 timer 时,用工厂创建 Hook 实例。SetCond(cond):设置调度条件,runtime 只调度匹配条件的 timer;不设置则调度所有 timer。Build()/Start()/Stop():构建、启动、停止。Start启动后台调度循环(pkg/timer/runtime/runtime.go#L129-L141)。
4.2 Runtime 内部工作原理
从 pkg/timer/runtime/runtime.go 的loop方法(L165-L242)可以看到,runtime 启动后运行一个主事件循环,协调以下机制:
- 全量刷新:每
fullRefreshTimersInterval(1 分钟)从 store 全量拉取匹配cond的 timer 更新本地缓存(fullRefreshTimers); - watch 增量更新:若 store 支持 watch(
WatchSupported()),通过store.Watch(ctx)订阅变更,按batchProcessWatchRespInterval(1 秒)批量处理 Create/Update/Delete 事件,实现近乎实时的增量刷新; - 触发检测:
tryTriggerEventTimer定时尝试触发到期事件。触发间隔被限制在minTriggerEventInterval(1 秒)到maxTriggerEventInterval(60 秒)之间,防止空转消耗 CPU; - hook worker:每个 HookClass 对应一个
hookWorker(pkg/timer/runtime/worker.go),串行处理该 HookClass 下所有 timer 的触发请求,请求通过带缓冲 channel(容量 128)投递; - worker 响应处理:worker 完成触发后通过
workerRespCh回传结果,runtime 据此更新缓存中的处理状态(procTriggering→procWaitTriggerClose→procIdle); - panic 恢复:整个循环与 worker 循环都包裹在
withRecoverUntil中,panic 后延迟retryLoopWait(10 秒)自动重启。
4.3 分布式注意事项
如果启动多个调度条件重叠的 runtime,同一 timer 的 Hook 有概率被调用多次。因此生产环境最好用分布式锁确保同一时刻只有一个 runtime 在运行。文档同时说明:未来 TiDB 系统会提供"保留 runtime"(preserved runtimes),届时可直接使用而无需自行创建。
五、自定义 Hook 类:实现触发动作
Hook 是触发动作的载体。实现api.Hook接口即可(接口定义见 pkg/timer/api/hook.go#L42-L56):
func MyTimerHookFactory(hookClass string, cli api.TimerClient) api.Hook { return &MyTimerHook{ cli: cli, } } type MyTimerHook struct { cli api.TimerClient } func (h *MyTimerHook) Start() { // You should do some init works here. } func (h *MyTimerHook) Stop() { // You should do some clear works here. } func (h *MyTimerHook) OnPreSchedEvent(ctx context.Context, event api.TimerShedEvent) (api.PreSchedEventResult, error) { fmt.Printf("OnPreSchedEvent: %s\n", event.EventID()) return api.PreSchedEventResult{}, nil } func (h *MyTimerHook) OnSchedEvent(ctx context.Context, event api.TimerShedEvent) error { fmt.Printf("OnSchedEvent: %s, event start: %s\n", event.EventID(), event.Timer().EventStart) return h.cli.CloseTimerEvent(ctx, event.Timer().ID, event.EventID()) }5.1 生命周期与调用时序
- 工厂函数
MyTimerHookFactory用于RegisterHookFactory。runtime 需要时为某个 HookClass 创建 Hook 实例,并传入一个api.TimerClient,Hook 可用它做进一步的 timer 操作(如关闭事件、更新 watermark)。 - 同一 HookClass 的所有 timer 共享同一个 Hook 实例。
- runtime 发现某个 timer 到期后,先查找对应 Hook 是否已创建;没有则调用工厂创建,然后调用
Start()做初始化——Start()只调用一次。 - 之后每次触发前都会调用
OnPreSchedEvent做预触发处理。 OnPreSchedEvent返回PreSchedEventResult告知 runtime 下一步动作(结构体见 pkg/timer/api/hook.go#L30-L40):大多数情况返回空结果继续触发;也可以设置Delay延迟,或设置EventData携带额外数据。
5.2 延迟调度(窗口限制等场景)
业务上有时需要限制调度窗口,例如某些时段不允许触发。可以在OnPreSchedEvent中返回延迟:
func shouldDelaySchedule() bool { // your function to check whether to delay a timer's schedule } func (h *MyTimerHook) OnPreSchedEvent(ctx context.Context, event api.TimerShedEvent) (api.PreSchedEventResult, error) { if shouldDelaySchedule() { return api.PreSchedEventResult{ Delay: time.Minute, }, nil } return api.PreSchedEventResult{}, nil }延迟 1 分钟后,OnPreSchedEvent会被再次调用;只有当它返回的Delay为 0 时,触发动作才会继续。worker 内部正是通过result.Delay > 0判断,并WithRetryAfter(result.Delay)安排重试(pkg/timer/runtime/worker.go#L336-L339)。
5.3 携带自定义事件数据
若想给触发动作带些额外数据,可设置PreSchedEventResult.EventData:
func (h *MyTimerHook) OnPreSchedEvent(ctx context.Context, event api.TimerShedEvent) (api.PreSchedEventResult, error) { if shouldDelaySchedule() { return api.PreSchedEventResult{ EventData: []byte("mycustomeventdata"), }, nil } return api.PreSchedEventResult{}, nil }EventData会在动作真正触发前被持久化到 store,因此 TiDB 进程重启后可以基于它做恢复工作(预计算的事件配置不会丢失)。该数据在 worker 的buildEventUpdate中通过update.EventData.Set(result.EventData)写入,并附带EventStart与EventWatermark等额外信息(pkg/timer/runtime/worker.go#L431-L450)。
5.4 事件状态流转与事件关闭
PreSchedEventResult指示继续触发后,事件元数据(含EventData)会被持久化到 store。之后用 client 查询该 timer,会看到EventStatus从IDLE变为TRIGGER,且EventID非空。
随后调用 Hook 的OnSchedEvent,在这里做真正的触发动作——发消息队列、提交外部作业,甚至直接执行轻量任务都可以。注意以下语义:
OnSchedEvent返回 nil(无错误)时只会被调用一次;- 只有两种情况会再次调用
OnSchedEvent:OnSchedEvent返回了 error;- runtime 重启后发现 timer 的
EventStatus是TRIGGER。
你必须调用 client 的CloseTimerEvent关闭事件,把状态重置为IDLE,否则该 timer 无法再调度新事件:
func (h *MyTimerHook) OnSchedEvent(ctx context.Context, event api.TimerShedEvent) error { // do some things ... // close the timer event so that the next event can be scheduled return h.cli.CloseTimerEvent( ctx, event.Timer().ID, event.EventID(), api.WithSetWatermark(time.Now()), // use the current time as the next watermark instead of the event start time ) }CloseTimerEvent的实现细节(pkg/timer/api/client.go#L241-L268)值得注意:
- 默认将
Watermark重置为EventStart的值,但你可以通过WithSetWatermark手动指定下一个 watermark(上例改为当前时间); - 内部做了严格校验:关闭事件时只允许同时更新 watermark 和 summary 字段,其他字段(如 EventStatus、EventID、EventData、EventStart)由框架强制改写;
- 通过
CheckEventID保证只关闭指定的那一次事件(防止关闭了更新后的事件)。
5.5 消息投递语义
结合上面的机制可以推导出框架支持的投递语义:
- at-most-once(至多一次):
OnSchedEvent成功(返回 nil)后只调用一次,事件随即被关闭; - at-least-once(至少一次):
OnSchedEvent返回 error 时框架会自动重试;进程重启后若发现EventStatus == TRIGGER也会重新触发——这就是"至少一次"的来源,因此业务处理逻辑要具备幂等性; - 若想在单次事件中同时保证不丢、不重,通常是在
OnSchedEvent中配合外部事务/幂等键自行实现 exactly-once。
六、故障恢复与 FAQ 深入解读
6.1 进程重启 / 长时间停机时框架的行为
TiDB 重启意味着 runtime 重启。重启后 runtime 会从 store 重新加载所有 timer 并检查状态(对应 runtimeStart后先执行fullRefreshTimers的逻辑):
- 若 timer 的
EventStatus是TRIGGER,runtime 会再次调用 Hook 的OnSchedEvent尝试触发该事件(不再调用OnPreSchedEvent); - 若
EventStatus是IDLE,则按调度策略判断是否到触发时间。
长时间停机导致的"漏触发"只会补一次。例如某 timer 的 cron 表达式为0 * * * * *(每小时触发一次),TiDB 在 watermark 为2022-01-01 09:58:00时停机,2022-01-01 12:01:00才恢复。期间有 3 个触发点被错过,但 Hook 只会被调用一次,EventStart为2022-01-01 12:01:00,watermark 仍为2022-01-01 09:58:00。如何处理取决于 Hook 自身:
- 可以在一次
OnSchedEvent调用里补做 3 次动作; - 也可以只做一次,然后用新 watermark
2022-01-01 10:00:00关闭事件。此时由于下一个预期触发时间11:00:00仍在当前时间之前,timer 会在极短时间内再次被触发。
6.2 ID 与 Key 的区别
两者都唯一,但含义不同:
Key由用户指定,用于语义化操作(查询、幂等创建等);它不是绝对唯一——不同 namespace 可以存在相同 key,同一 namespace 下删除后的 key 也可以被重新创建。ID由框架生成,是定时器的"真实标识",绝对唯一:任何两个 timer 不可能有相同 ID,即使其中一个被删除。因此记录/追踪事件日志时应优先使用ID。
6.3 Hook 方法的线程安全性
Start、Stop、OnPreSchedEvent、OnSchedEvent目前在同一 goroutine 中串行调用,是线程安全的,仅在这些方法中读写(且不额外开 goroutine)的变量不会发生数据竞争。但如果你手动创建 goroutine 在后台访问这些变量,仍需自行加锁。未来OnPreSchedEvent、OnSchedEvent可能对不同 timer 并发调用,但框架仍保证同一 timer 的这些方法按顺序调用,满足 happens-before 语义。
6.4 Hook 调用是否会互相阻塞
会。共享同一 Hook 的多个 timer 在同一个 goroutine 中触发(未来可能是固定数量 goroutine 的池)。若某个 timer 的 Hook 调用很慢,会阻塞其他 timer 的触发。解决办法:把真正的触发动作放到后台异步执行,前台调用立即返回非错误值。注意此时失败重试要自己负责(前台已返回成功,框架不会重试)。
七、生产实践:TTL 模块如何落地这套框架
TiDB 自身的TTL(数据过期清理)模块是这套框架的首个生产级使用者,位于 pkg/ttl/ttlworker/timer.go。
从 pkg/ttl/ttlworker/timer.go#L273-L280 可以看到,TTL 模块通过NewTimerRuntimeBuilder("ttl", store)构建了一个名为ttl的 runtime,然后:
- 用
SetCond(&timerapi.TimerCond{Key: timerapi.NewOptionalVal(timerKeyPrefix), KeyPrefix: true})让 runtime 只调度 TTL 自己的定时器(key 前缀过滤); - 用
RegisterHookFactory(timerHookClass, ...)注册 TTL 专用的 Hook 工厂,Hook 内部通过adapter将定时器触发转化为 TTL 作业提交; - 事件关闭时不仅更新 watermark,还会写入 TTL 任务的汇总信息(
WithSetSummaryData(summaryData),见 pkg/ttl/ttlworker/timer.go#L246)。
这与本文第 5.4 节介绍的CloseTimerEvent用法完全一致,是一个很好的端到端参考实现。
八、监控与运维
Timer framework 内置 Prometheus 指标定义于 pkg/timer/metrics/metrics.go。从源码可以看出两类核心指标:
- runtime 级:
full_refresh_timers、partial_refresh_timers(前缀runtime.<groupID>),用于观察 runtime 从 store 加载定时器的频率; - hook worker 级:
trigger、OnPreSchedEvent、OnPreSchedEvent_error、OnPreSchedEvent_delay、OnSchedEvent、OnSchedEvent_error(按 HookClass 维度),可用于监控每个 Hook 的调用量、错误率与延迟频次。
结合运行时日志(runtime 与 worker 均使用logutil.BgLogger,带groupID与hookClass标签),可以完整观测定时器的调度健康度。
九、快速上手清单
- 确定需求是否符合框架定位:周期性 + 分布式唯一执行 + 需要持久化调度状态;触发动作轻量。
- 创建 store:测试用
api.NewMemoryTimerStore();生产用tablestore.NewTableTimerStore(...)(基于 TiDB 系统表)。 - 创建 client:
api.NewDefaultTimerClient(store)。 - 创建 timer:
client.CreateTimer(ctx, api.TimerSpec{...}),配置 Key、SchedPolicyType/Expr、HookClass、Enable 等。 - 实现 Hook:实现
api.Hook的Start/Stop/OnPreSchedEvent/OnSchedEvent,并在OnSchedEvent末尾调用client.CloseTimerEvent关闭事件。 - 搭建 runtime:
runtime.NewTimerRuntimeBuilder(groupID, store).RegisterHookFactory(...).SetCond(...).Build()后Start()。 - 分布式部署时用分布式锁保证只有一个 runtime 在运行;Hook 逻辑保持幂等以配合 at-least-once 语义。
更多细节可继续阅读框架的单元测试与集成测试,例如 pkg/timer/api/timer_test.go、pkg/timer/runtime/runtime_test.go 以及 pkg/timer/store_intergartion_test.go,它们展示了各类边界行为的预期行为。
【免费下载链接】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),仅供参考