从设计到落地:TiDB 如何用事务断言与 Mutation Checker 治理数据索引不一致

📅 发布时间:2026/9/10 16:39:50
从设计到落地:TiDB 如何用事务断言与 Mutation Checker 治理数据索引不一致
从设计到落地TiDB 如何用事务断言与 Mutation Checker 治理数据索引不一致【免费下载链接】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数据与索引不一致Data Index Inconsistency是 TiDB 生产环境中最难排查、影响最恶劣的数据问题之一轻则写入报错、重则数据“悄悄变错”甚至丢失。本文以仓库设计文档 docs/design/2021-09-22-data-consistency.md 为骨架结合当前仓库源码完整讲解 TiDB 提出的两层治理方案——事务断言Txn Assertion与Mutation Checker变更一致性检查——包括其背后的两个系统变量、断言下推原理、悲观事务的 FAST/STRICT 策略、实现源码路径与测试设计。读完本文你将理解这两个内部开关各自防什么、怎么防、各自的开销与适用场景以及如何基于源码验证其行为。一、要解决的问题数据索引不一致为什么如此可怕TiDB 使用行数据与索引分离存储的架构同一行记录的行数据Row Data与索引Index在数量或内容上不一致的情况偶尔会发生。设计文档docs/design/2021-09-22-data-consistency.md开篇即点明了它对生产用户的恶劣影响主要包括三类读错误Read errors读写不一致数据时直接报错数据错误Data error数据可以读取但读到的结果是错误的数据丢失Data Loss已经写入的数据读不出来。更棘手的是排查环节。支持工程师定位这类问题时普遍面临三大困难诱因众多可能导致数据错误的原因非常多难以一一排查现象无法反推原因不一致的现象本身很难直接对应到某个特定成因取证困难问题发生时可用于调查的证据信息难以收集。问题影响大、排障难这正是当时2021 年投入资源做专项改进的核心动因。而针对这一目标方案采取了“预防 检测”的双层手段分别由两个系统变量控制。二、整体方案与两个系统变量方案提出两种减少数据损坏发生与扩散的手段手段对应系统变量取值默认值作用时机事务断言Assertiontidb_txn_assertion_levelOFF/FAST/STRICT视内核/部署而定见下文写入之前在 TiKV 侧校验阻止不一致写入发生变更一致性检查Mutation Checkertidb_enable_mutation_checkerON/OFFOFF默认关闭事务提交前在 TiDB 侧复查尽早暴露已存在的问题数据、防止错误扩散在 pkg/sessionctx/vardef/tidb_vars.go 中可以找到两个变量名的正式定义其注释明确写到tidb_txn_assertion_level用于“检测并预防数据与索引不一致问题detect and preventing data index inconsistency problems”。系统变量注册与使用方式两个变量在 pkg/sessionctx/variable/sysvar.go 中注册从源码可以看到它们的关键属性作用域ScopeGlobal | ScopeSession即既支持全局设置也支持会话级覆盖均可通过SET语句调整SET GLOBAL tidb_txn_assertion_level FAST;SET SESSION tidb_enable_mutation_checker ON;类型tidb_enable_mutation_checker为布尔类型BoolToOnOff底层写入SessionVars.EnableMutationCheckertidb_txn_assertion_level为枚举类型TypeEnumPossibleValues严格限定为OFF/FAST/STRICT底层写入SessionVars.AssertionLevel默认可见性两者在系统变量表中均被标记为Hidden: true属于面向内部与诊断场景的开关不通过普通的变量罗列对外暴露。关于默认值需要说明两点均以当前仓库源码为准在 pkg/sessionctx/vardef/tidb_vars.go 中DefTiDBEnableMutationChecker false、DefTiDBTxnAssertionLevel AssertionOffStr即最朴素的出厂默认是Mutation Checker 关闭、断言级别 OFF但代码中还提供了按部署形态决定默认值的逻辑函数GetDefaultTxnAssertionLevel()pkg/sessionctx/vardef/tidb_vars.go会根据内核类型返回不同的默认级别部分内核场景下会直接返回STRICT同时在 pkg/sessionctx/variable/sysvar.go 的特殊取值处理中存在针对 next-gen 内核等部署把断言默认设为STRICT/FAST、并把 Mutation Checker 默认置为ON的分支。也就是说具体集群实际生效的默认值取决于其内核类型与初始化方式不能一概而论建议以实际部署中查询到的值为准。为什么需要两道防线断言Assertion解决的是“别写入错误数据”把对 KV 存在性的预期下推给 TiKV在写入生效前拦截Mutation Checker解决的是“别让已有问题继续传播、扩散成更大问题”当事务执行器自身出现潜在 bug、写入了不一致的 KV 变更时在提交前把它拦下来并给出可诊断的报错。二者互补断言在“源头”上防患于未然检查器在“出口”上兜底止损。三、第一道防线事务断言Assertion的机制与源码印证3.1 基本思想把“算子已知的必然事实”变成检查SQL 执行器在执行 DML如UpdateExec、InsertExec时基于算子的行为特征、并结合它已经做过的检查例如唯一索引冲突检测天然可以推断出某些数据在被修改时应当满足的约束。例如执行UPDATE修改某行主键/唯一索引对应 KV 时该 Key 在存储中必然已存在执行INSERT写入新记录时正常情况下其对应的 Key 在存储中必然不存在。这些“必然成立”的约束就是断言Assertion。断言随 DML 操作写入 MemBuffer内存中模拟事务内写入的 memDB并最终下推到 TiKV在真正落盘写入前执行校验。3.2 四种断言标志Assertion FlagsDML 算子对 memDB 的修改本质上只有两类 KV 操作Put KV新增/更新键值对与Del KV删除键值对。方案为每条被修改的 KV 额外附加一组断言标志Assertion Flags设计文档给出共占用2 个 bit的四种取值标志位编码语义是否允许被事务内后续修改覆盖None00当前对该 KV 没有断言允许覆盖默认标志Unknown11该 KV 无法被断言不允许覆盖MustExist10Key 在存储中必须已存在不允许覆盖MustNotExist01Key 在存储中必须不存在不允许覆盖Unknown与None的区别值得强调None只是“暂时没想好”后续语句还能补上更强断言而Unknown是“确实无法断言”例如 KV 来自无法推断来源的写入一旦确定就封死事务内任何后续修改都不能再把该 KV 改成更强的存在性断言以免做出错误承诺。3.3 标志的生命周期与语句级回滚语义断言标志在 MemDB 中与 KV 数据同生命周期管理遵循严格的语句级statement阶段规则标志随 KV 一起先被设置到当前语句的执行阶段若该语句执行报错并回滚则这些标志随之失效被丢弃若语句执行成功并最终提交标志才在事务中真正生效。这样设计保证了“失败的尝试不会留下错误的断言痕迹”。3.4 在 2PC 提交阶段下推 TiKV当事务进入两阶段提交2PC时断言信息会在prewrite 阶段随请求一起发送给 TiKV。TiKV 在真正修改数据之前会根据断言额外执行存在性检查一旦断言失败TiKV 直接拒绝该请求从而阻止不一致数据进入存储。3.5 悲观事务用 lock 请求缓存存在性信息FAST 与 STRICT 两种兜底对悲观事务而言prewrite 请求可能不会重新读取 Key 的历史值因此需要在别处获取存在性信息。方案的做法是让悲观锁请求pessimistic lock request在返回时携带该 Key 的存在性信息客户端TiDB 侧把这份信息缓存起来在事务提交前TiDB 侧基于缓存信息自行校验断言从而避免在 TiKV 中额外增加读开销。但并非悲观事务中的每个 Key 都会被加悲观锁针对这些“未加锁”的 Key方案设计了两种策略策略行为特点FAST直接忽略这些未被悲观锁覆盖的 Key几乎不影响性能但存在断言覆盖不到的盲区STRICT在 TiKV 中读取全部 Key进行完整断言覆盖最全面但会引入额外的读与开销这也是tidb_txn_assertion_level三档取值的本质含义OFF完全关闭FAST只做不影响性能的断言STRICT做完整断言、即使会拖慢性能也在所不惜。三者注释可分别在 pkg/sessionctx/vardef/tidb_vars.go 的AssertionStrictStr/AssertionFastStr/AssertionOffStr常量上看到。3.6 当前仓库中的断言实现印证设计文档中的 4 种标志在当前代码库中被实现为kv.AssertionOp枚举pkg/kv/assertion.goAssertExist—— 对应设计中的MustExistKey 必须存在AssertNotExist—— 对应MustNotExistKey 必须不存在AssertUnknown—— 对应Unknown无法断言AssertNone—— 对应None无操作。其中AssertUnknown的编码方式很能体现语义它在 KeyFlags 上同时置位“断言存在”与“断言不存在”两个标志位见 pkg/kv/assertion.go即“说它存在也不对、说它不存在也不对”从而锁死该 Key 不再接受后续覆盖——与设计文档中Unknown(11)不允许被覆盖的表述完全吻合。同时代码把AssertionOp与通用的标志操作类型分开定义注释专门强调这是“为防止误用断言标志而单独设立的类型”。断言被应用到事务内存缓冲时经由 pkg/table/tables/assertion.go 的setAssertion辅助函数完成它先读取 MemBuffer 中 Key 的现有标志若该 Key 已带有断言标志则直接保留、不做覆盖呼应“Unknown 等不允许被覆盖”的设计否则调用UpdateAssertionFlags写入新断言。四、第二道防线Mutation Checker变更一致性检查4.1 基本思想把“一致性”在事务边界上传递下去设计文档对 Mutation Checker 给出了一条非常清晰的公理式表述当事务执行前数据是一致的就要确保事务不会写出不一致的数据。用集合的语言可以更精确地刻画。设事务执行前后行的值集合分别为 V1 与 V2且事务执行前数据与索引是一致的那么事务提交后数据仍然一致的条件等价于V1 − V2 { Del_index.value } 事务中删除的行值集合必须恰好等于被删除的索引项值 V2 − V1 { Put_index.value } 事务中新增的行值集合必须恰好等于被写入的索引项值也就是说行数据上发生的新增/删除必须与索引上发生的新增/删除一一对应、值也一致。只要在提交前验证这一等式成立就能在“前置数据一致”的前提下保证“提交后数据依然一致”从而把执行器可能引入的错误拦截在事务边界内防止错误数据扩散、并让排查者第一时间获得明确报错。4.2 执行时机与触发路径Mutation Checker 并不扫描全表而是聚焦于本事务自己产生的变更在执行器向 memDB 写入、准备提交之前从 MemBuffer 的当前语句/staging 阶段收集本事务对行与索引的全部 KV 变更再交叉比对。实现集中在 pkg/table/tables/mutation_checker.go核心入口是CheckDataConsistency(txn, tc, t, newData, oldData, memBuffer, sh)mutation_checker.go接收事务、表上下文、新旧行数据与 memBuffer/staging 句柄是检查的总入口checkHandleConsistencymutation_checker.go核对行插入与各索引变更在 handle含聚簇索引主键层面是否自洽checkIndexKeysmutation_checker.go与checkRowInsertionConsistencymutation_checker.go逐索引比对新增 KV 与行数据collectTableMutationsFromBufferStagemutation_checker.go从 MemBuffer 的 staging 阶段收集该表产生的全部变更正好对应“语句级缓存、提交前生效”的生命周期CompareIndexAndValmutation_checker.go在给定排序规则collator下比较索引值与行列值是否一致支持多值索引cmpMVIndex场景getColumnMaps/getOrBuildColumnMapsmutation_checker.go构建并缓存行列、索引列之间的映射用于高效比对。在 pkg/table/tables/tables.go 与 pkg/table/tables/tables.go 可以看到检查器被挂接在行数据写入的关键路径上对应AddRecord/UpdateRecord一类操作每当向表中新增或更新一行、并同步产生索引变更后都会调用CheckDataConsistency做提交前复查。这就保证了行侧变更与索引侧变更在同一份事务缓冲内被一次性核对。4.3 它能发现什么从检查点覆盖的范围看Mutation Checker 能发现的问题包括但不限于行新增了但某个索引漏写、多写或值不一致行删除时索引项未同步删除对应公式中的V1 − V2侧不匹配因排序规则collation、行格式、字符集、时区等编码相关因素导致的索引值与列值在字节层面的不一致聚簇索引、前缀索引、表达式索引、多值索引等复杂表结构下的索引写入错误。值得一提的是mutation_checker.go 中还存在corruptMutations与injectMutationError这类函数——它们是测试用的故障注入工具用于人为构造不一致的变更以验证检查器能否如期拦截可见该功能在研发阶段就配套了针对性的正确性验证手段对应 mutation_checker_test.go。五、测试设计正确性、有效性与兼容性如何被保证该特性的质量目标包括正确性correctness、有效性effectiveness、效率efficiency、易用性ease of use与兼容性compatibility测试相应分为两大块保证正确性含兼容性数据正确时不得误报no false positives度量有效性能拒绝携带不一致变更的事务对已存在的坏数据尽早报错阻止错误扩散。回归测试Regression Tests所有回归测试都不应触发数据一致性报错——因为正常路径下数据本就一致若回归用例大量误报说明检查器本身存在缺陷。因此回归测试被用来长期守护“零误报”这一正确性底线。系统测试System Tests系统测试需要专门构造覆盖以下维度的用例它们恰好也代表了检查器最容易踩坑的领域编码相关Encoding排序规则Collation、行格式Row Format、字符集Charset、时区Time Zone表结构相关Table structure并发 DDL、聚簇索引Clustered Index、前缀索引Prefix Index、表达式索引Expression Index。在仓库中可找到大量与这些开关配套的实测用例例如 tests/realtikvtest/txntest/txn_test.go 与 tests/realtikvtest/pessimistictest/pessimistic_test.go 中均有针对tidb_enable_mutation_checker/tidb_txn_assertion_level的集成验证pkg/ddl/tests/partition/multi_domain_test.go 等 DDL 相关测试也会显式设置这两个开关确保新特性与并发 DDL 场景兼容。性能测试Performance Tests特性设计目标是对性能影响“尽可能小”衡量维度包括延迟、吞吐、内存与 CPU 消耗需要跑标准的sysbench 与 TPC-C基准。六、影响与风险这些开关不是免费的根据设计文档对性能测试的预期需要向使用者如实说明两类主要开销STRICT断言模式的悲观事务开销会带来调度器schedulerCPU 占用的明显上升以及 kvdbget与seek操作次数的增加——这正是STRICT需要“读取全部 Key”的代价Mutation Checker 的 CPU 开销检查器会提升 TiDB 侧 CPU 占用。设计文档给出的预期量化结果是当CPU 成为 TiDB 瓶颈时开启该特性可能使吞吐下降约1.5%、P95 延迟上升约2.4%。注以上百分比数字来自设计文档 docs/design/2021-09-22-data-consistency.md 中的“Impacts Risks”章节属于方案设计阶段的预期评估用于指导功能默认值的取舍与上线策略实际收益与开销请以真实负载下的压测结果为准。这也解释了两个变量的默认取向Mutation Checker 默认关闭tidb_txn_assertion_level仅在性能可接受的部署形态下默认开启/调高就是为了在数据安全收益与性能开销之间取得平衡。七、使用建议与诊断价值小结综合设计与实现可以给出如下操作层面的结论默认不要盲目全开。先确认所在集群内核类型的实际默认值两变量的默认值逻辑受内核类型影响见 pkg/sessionctx/vardef/tidb_vars.go再决定是否手动调整怀疑索引/数据不一致时可将tidb_txn_assertion_level临时调至STRICT、并开启tidb_enable_mutation_checker用略高的开销换取更强的写入拦截与提交前检出能力让问题在事务边界被显式报出而不是静默扩散两者作用阶段不同断言在 TiKV 写入前拦截防患检查器在 TiDB 提交前复查止损配合使用才能构成完整闭环该特性同时改进了不一致问题的日志与报错能力正是为了缓解文档开头所述“取证困难”这一核心痛点——一旦检查器或断言命中运维与支持人员能拿到明确指向问题 KV 的报错而不是面对一堆无法归因的异常数据。从 2021 年的设计提案到当前仓库中 pkg/kv/assertion.go、pkg/table/tables/assertion.go、pkg/table/tables/mutation_checker.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),仅供参考