TiDB Lock View 锁视图设计解析:基于 information_schema 的事务锁等待、锁竞争与死锁诊断 TiDB Lock View 锁视图设计解析基于 information_schema 的事务锁等待、锁竞争与死锁诊断【免费下载链接】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导读Lock View 是 TiDB 提供的一套基于information_schema内存表的锁诊断体系用于分析事务的锁等待Lock Waiting、锁竞争Lock Contention与死锁Deadlock问题。本文围绕设计文档 docs/design/2021-04-26-lock-view.md 展开结合仓库内pkg/infoschema、pkg/session/txninfo、pkg/util/deadlockhistory、pkg/config等源码实现系统讲解TIDB_TRX、DATA_LOCK_WAITS、DEADLOCKS、TRX_SUMMARY等表的结构与语义、TiDB/TiKV 之间为支持该功能而引入的协议改动以及 5 个可动态调整的配置项。读完本文你将掌握锁视图各表的字段含义、权限模型与典型使用场景并能在定位线上锁冲突与死锁时快速上手。一、设计动机为什么需要 Lock View在引入锁视图之前分析 TiDB 中的锁竞争与死锁极其困难。设计文档 docs/design/2021-04-26-lock-view.md 的 Motivation 一节指出了当时的主要痛点只能开启 general log尝试复现问题再从日志中人工分析过程繁琐且难以在真实场景落地即便拿到了日志许多冲突信息只包含一个start_ts事务起始时间戳无法提供诸如事务中执行的 SQL 等可用于复现的有用信息部分场景下这种日志考古式的分析方式根本不具备可行性。因此需要一个更好的手段来回答三类问题谁在等锁、锁被谁持有、死锁是如何形成的。Lock View 的答案是把这些诊断信息以内存表的形式暴露给information_schema让用户可以直接用 SQL 查询。二、总体设计内存表 本地/集群双版本设计文档给出的核心方案是提供若干张位于information_schema中的表。其中部分表同时提供本地版本只读取当前 TiDB 节点数据与集群版本聚合整个 TiDB 集群的数据集群版本的表名带有CLUSTER_前缀。这一约定在当前仓库的 pkg/infoschema/cluster.go 中得到落实例如CLUSTER_TIDB_TRX见ClusterTableTiDBTrx常量CLUSTER_DEADLOCKSCLUSTER_TRX_SUMMARY在 pkg/infoschema/tables.go 中这些表被注册为带自增表 ID 的系统表如TableTiDBTrx TIDB_TRX对应autoid.InformationSchemaDBID 70并各自声明了列定义tableTiDBTrxCols、tableDeadlocksCols、tableDataLockWaitsCols、tableTrxSummaryCols。设计上还明确了两条通用原则数据只存内存、无需持久化事务相关数据具有时效性查询它们通常是短时排障因此采用内存表实现查询整个集群时将本地表注册到infoschema/cluster.go并拼接出带前缀的全局表。权限控制访问这些表的完整内容需要PROCESS权限无权限用户只能看到当前用户发起的事务其余会被过滤行为与processlist表类似。三、核心表结构详解3.1(CLUSTER_)TIDB_TRX运行中事务一览该表描述当前正在执行的事务。设计文档定义的核心字段如下FieldTypeCommentTRX_IDunsigned bigint事务 ID即 start tsTRX_STARTEDtime事务起始时间人类可读CURRENT_SQL_DIGESTvarchar(64)当前正在执行的 SQL 语句的 digestALL_SQL_DIGESTStext事务已执行过的所有 SQL 语句 digest 列表STATEenum(Running,Lock waiting,Committing,RollingBack)事务所处状态WAITING_START_TIMEtime当前锁等待开始后经过的时间若有等待SCOPEenum(Global,Local)事务作用域ISOLATION_LEVELenum(REPEATABLE-READ,READ-COMMITTED)隔离级别AUTOCOMMITbool是否自动提交SESSION_IDunsigned bigint所属会话 IDUSERvarchar用户名DBvarchar数据库名SET_COUNTint当前事务修改的 key 数LOCKED_COUNTint当前事务加锁的 key 数MEM_BUFFER_KEYSint事务 membuffer 中的条目数MEM_BUFFER_BYTESint事务 membuffer 占用的字节数行生命周期事务首次执行写操作或加锁操作时创建一行事务结束后删除。数据收集与存储所有信息都可在 TiDB 侧收集。由于并发事务数量不会过大、且无需持久化非常适合做成内存表。实现上多数信息可以参照ProcessInfo的传递方式获得甚至直接复用ProcessInfo结构。在 pkg/session/txninfo/txn_info.go 中TxnInfo结构体即被明确注释为TIDB_TRX表的数据源data source负责跨线程安全地暴露StartTS、CurrentSQLDigest、AllSQLDigests等字段。实际实现相对设计文档的演进对照 pkg/infoschema/tables.go 中tableTiDBTrxCols的列定义可以发现落地后的列与最初设计有所差别主要包括补充了CURRENT_SQL_DIGEST_TEXT当前 SQL 的规范化文本、ALL_SQL_DIGESTS变更为 blob 类型、新增RELATED_TABLE_IDS事务访问过的表 ID 列表与WAITING_TIME当前锁等待时长等列STATE枚举在实现中为Idle, Running, LockWaiting, Committing, RollingBack见 pkg/session/txninfo/txn_info.go 的TxnRunningStateStrs相比设计稿增加了Idle状态且锁等待写作LockWaitingALL_SQL_DIGESTS这类 digest 列可通过tidb_decode_sql_digests()函数还原为可读 SQL在 pkg/infoschema/test/clustertablestest/tables_test.go 中即有select tidb_decode_sql_digests(all_sql_digests) from information_schema.tidb_trx的用例。3.2DATA_LOCK_WAITS当前锁等待快照该表描述当前正在发生的锁等待。注意它没有(CLUSTER_)双版本因为其数据天然来自 TiKV 侧本身即是全局的FieldTypeCommentKEYvarchar正在等待的 keyTRX_IDunsigned bigint当前正在等待锁的事务SQL_DIGESTvarchar(64)正在尝试获取锁的 SQL 的 digestCURRENT_HOLDING_TRX_IDunsigned bigint持有锁、阻塞当前事务的那个事务行生命周期一个锁等待进入 LockManager 时创建对应行离开 LockManager 时删除。因此这是一张当前正在等锁的快照表。数据收集与存储数据全部在TiKV 的 LockManager上收集为此需要新增一个 RPC 入口供 TiDB 查询。由于原 LockManager 不保存未哈希的 key 与 SQL digest设计上需要对其加以改造。设计取舍当前持锁事务的 SQL digest 对排查很有价值但在当时架构下实现成本过高因此不会包含在该功能的第一版中。此外设计文档在 Unresolved Questions 中也坦承由于 TiKV 侧的锁等待可能超时并重试单次查询DATA_LOCK_WAITS未必能看到全部逻辑意义上的锁等待。对照 pkg/infoschema/tables.go 的tableDataLockWaitsCols实现中同样补充了KEY_INFOkey 的描述信息便于可读与SQL_DIGEST_TEXT等待 SQL 的规范化文本两列。3.3(CLUSTER_)DEADLOCKS死锁事件历史与上述两张实时快照表不同(CLUSTER_)DEADLOCKS保存的是历史死锁事件一个事件可能占用多行FieldTypeCommentDEADLOCK_IDint单个死锁事件需要多行共同表示该字段用于区分不同事件OCCUR_TIMEtime死锁发生的物理时间RETRYABLEbool该死锁是否可重试TiDB 会尝试判断当前语句是否间接在等待一个由当前语句自己加的锁TRY_LOCK_TRX_IDunsigned bigint正在尝试获取锁的事务 IDstart tsCURRENT_SQL_DIGESTtext被阻塞的 SQLKEYvarchar正被死锁事件中另一事务持有、导致无法加上的锁 keyALL_SQL_DIGESTStext该事务已执行 SQL 的 digest 列表TRX_HOLDING_LOCKunsigned bigint当前持有锁的事务该事务会以相同的DEADLOCK_ID在表中出现另一行行生命周期TiDB 收到死锁错误后创建对应行缓冲区写满后按FIFO先进先出淘汰最旧记录。在实现中pkg/util/deadlockhistory/deadlock_history.go 的NewDeadlockHistory(capacity)通过环形数组实现容量受限的历史缓冲并通过Resize支持动态扩容/缩容。数据收集与存储死锁事件信息可全部在 TiDB 侧收集——收到来自 TiKV 的死锁错误时写入表即可死锁环中其他事务的信息则需要在处理死锁错误时从CLUSTER_TIDB_TRX表另行获取。同时 TiKV 需要在死锁错误中上报更丰富的信息见下文协议章节。重试型死锁的特殊处理内部存在两种死锁错误——可重试retryable与不可重试。事务对可重试死锁会在内部重试不会向客户端报错因此用户通常更关心不可重试的死锁可重试死锁默认不收集可通过配置开启见pessimistic-txn.deadlock-history-collect-retryable对可重试死锁也去采集CLUSTER_TIDB_TRX可能引入性能损耗是否采集需经测试后再定。在 pkg/infoschema/tables.go 的tableDeadlocksCols中实现又补充了CURRENT_SQL_DIGEST_TEXT与KEY_INFO列以增强可读性。3.4(CLUSTER_)TRANSACTION_SUMMARY与(CLUSTER_)TRANSACTION_ID_DIGEST长事务画像这两张表用于回答哪种事务容易产生冲突。设计上TRANSACTION_SUMMARY对同构事务做聚合TRANSACTION_ID_DIGEST则建立慢/易冲突事务 ID → 事务 digest的反向索引(CLUSTER_)TRANSACTION_SUMMARYFieldTypeCommentDIGESTvarchar(16)事务的 digest由ALL_SQL_DIGEST计算得到ALL_SQL_DIGESTtext该类型事务执行过的所有 SQL digest 组成的 json 数组行生命周期第一笔同类型事务结束后创建缓冲区满后按LRU最近最少使用淘汰依据TRANSACTION_ID_DIGEST中的存在时间。(CLUSTER_)TRANSACTION_ID_DIGESTFieldTypeCommentDIGESTvarchar(16)事务 digest由ALL_SQL_DIGEST计算TRX_IDbigint事务 ID即 start ts行生命周期事务结束后满足特定条件才创建受内存限制当前条件为该事务执行过慢、更可能与其它事务冲突缓冲区满后按 FIFO 淘汰最旧记录。两张表的数据都可在 TiDB 侧收集事务结束时把信息写入即可。作为旁证当前仓库中该能力以TRX_SUMMARY表的形式落地常量TableTrxSummary TRX_SUMMARY及其集群版本CLUSTER_TRX_SUMMARY定义于 pkg/infoschema/tables.go 与 pkg/infoschema/cluster.go对应列结构为tableTrxSummaryColsDIGEST 全部 SQL digest 列表表 ID 为autoid.InformationSchemaDBID 80/81。3.5 权限模型小结表需要 PROCESS 权限说明(CLUSTER_)TIDB_TRX是无权限时仅显示当前用户自己的事务DATA_LOCK_WAITS是数据源在 TiKV(CLUSTER_)DEADLOCKS是含历史信息(CLUSTER_)TRANSACTION_SUMMARY/(CLUSTER_)TRANSACTION_ID_DIGEST是聚合 索引四、TiDB ↔ TiKV 协议扩展kvproto要让锁视图工作TiDB 与 TiKV 之间需要传输额外信息因此设计文档在kvproto协议层面给出了如下扩展方案。deadlockpb死锁检测协议为WaitForEntry增加锁 key 与资源组标签字段为DeadlockResponse增加完整等待链message WaitForEntry { ... bytes key ...; // 被等待的锁 key bytes resource_group_tag ...; // 资源组标签 } message DeadlockResponse { ... repeated WaitForEntry wait_chain ...; // 完整等待链 }kvrpcpbKV RPC 协议在Context中增加resource_group_tag在Deadlock错误中携带等待链并新增获取锁等待信息的 RPC 消息message Context { ... bytes resource_group_tag ...; // 将 SQL digest及更多信息序列化后携带于此 } message Deadlock { ... repeated deadlock.WaitForEntry wait_chain ...; } message GetLockWaitInfoRequest { Context context 1; } message GetLockWaitInfoResponse { errorpb.Error region_error 1; string error 2; repeated deadlock.WaitForEntry entries 3; }要点归纳resource_group_tag字段复用该字段不只服务于锁视图设计上期望被另一功能Top SQL复用其同样需要在大多数事务型请求中携带 SQL digest。实际去向是从悲观锁请求Context中取出锁 key 与resource_group_tag附加到死锁检测请求上并将等待链加入死锁检测响应。新增 store 级 RPCGetLockWait用于从 TiKV 获取锁等待状态。它属于存储节点级别而非 region 级别请求定位上类似UnsafeDestroyRange及 Green GC 相关 RPC。请求可携带过滤选项以剔除用户不关心的信息但当时的内存表实现只允许 TiDB 全表扫描后再过滤文档注明这一点留待后续优化。死锁错误携带完整等待链等待链会加入PessimisticLock请求返回的Deadlock错误中这样死锁发生时完整等待链信息可一路传回 TiDB供其写入(CLUSTER_)DEADLOCKS表并补充相关事务信息。五、相关配置项均支持动态调整锁视图的行为由 5 个配置项控制均定义于 pkg/config/config.go 的PessimisticTxn与TrxSummary结构体中且在 pkg/config/config.toml.example 中给出了默认示例。所有配置都可通过 HTTP API动态修改无需重启节点。5.1pessimistic-txn.deadlock-history-capacity语义每个 TiDB 节点保留的最近死锁事件数量上限DEADLOCKS历史缓冲容量。取值范围0 ~ 10000默认值10。源码佐证字段DeadlockHistoryCapacity位于 pkg/config/config.go 的PessimisticTxn结构体注释明确指向information_schema.deadlocks表该值被用于构造 pkg/util/deadlockhistory/deadlock_history.go 中的环形 FIFO 缓冲。5.2pessimistic-txn.deadlock-history-collect-retryable语义是否将可重试死锁也收集进(CLUSTER_)DEADLOCKS表。取值0不收集或 1收集默认值0不收集。示例配置中以布尔值false书写见 pkg/config/config.toml.example。源码佐证字段DeadlockHistoryCollectRetryable bool定义于 pkg/config/config.go注释说明其控制语句内可重试死锁是否被收集。5.3transaction-summary.transaction-id-digest-capacity语义每个 TiDB 节点在transaction_id_digest中保留的事务数上限。取值范围0 ~ 100000默认值10000。5.4transaction-summary.transaction-id-digest-min-duration语义一个事务运行多久才会被记录进transaction_id_digest并进入trx_summary的计算考量。执行时长不足该阈值的事务不会入表内存有限只关心慢/易冲突事务。取值范围0 ~ 2147483647单位ms默认值1000即 1 秒。5.5transaction-summary.transaction-summary-capacity语义每个 TiDB 节点在trx_summary中保留的事务摘要数量上限。取值范围0 ~ 5000默认值500。源码佐证字段TransactionSummaryCapacity与TransactionIDDigestMinDuration位于 pkg/config/config.go 的TrxSummary结构体TrxSummary.Valid()会校验transaction-summary-capacity不得超过 5000否则返回错误transaction-summary.transaction-summary-capacity should not be larger than 5000。提示前两组键以pessimistic-txn.开头说明该功能与悲观事务的锁管理机制深度绑定后三组键以transaction-summary.开头服务于事务画像统计模块。六、兼容性、测试设计与风险6.1 兼容性设计预期该功能不与其它功能产生不兼容。唯一需要留意的场景是升级过程中集群内同时存在不同版本的 TiDB 节点时带CLUSTER_前缀的表查询可能报错由于锁视图通常由用户手动使用这不算严重问题因此文档认为无需为此做特殊处理。6.2 测试设计功能测试查询上文定义的表能得到正确结果。场景测试覆盖三类典型故障场景——存在锁竞争时该功能能帮助定位问题某条 SQL 被另一事务阻塞时该功能能帮助定位问题发生死锁时该功能能帮助还原死锁的形成过程。兼容性测试N/A。基准测试该功能不应在正常场景下引入明显性能回退低于 2%访问这些表不应增加并发普通查询的延迟。仓库中的集成测试可印证前两类场景例如 pkg/infoschema/test/clustertablestest/tables_test.go 通过真实建表、执行update后查询information_schema.tidb_trx校验事务的state、all_sql_digests等字段并验证对同一张表的并发查询结果一致性。6.3 风险与未决问题设计文档诚实列出了若干当时尚未解决的边界问题值得使用者了解其能力边界TiKV 上锁等待可能因超时而重试因此单次查询DATA_LOCK_WAITS未必覆盖全部逻辑锁等待第一版实现中可能不收集内部事务的信息TiDB 收到死锁错误后需要再去查询其它事务信息期间事务状态可能已变化因此(CLUSTER_)DEADLOCKS表中信息的准确性与完整性无法保证关于事务冲突的统计信息仍然不足TIDB_TRX与DATA_LOCK_WAITS不保留历史某些历史问题可能仍难回溯此时应借助(CLUSTER_)DEADLOCKS与事务摘要表等带历史性质的表。七、横向参照与设计思路总结在方案选型上文档列举了业内同类系统的做法作为参照MySQL提供data_locks与data_lock_waits表Oracle提供v$lock视图CockroachDB提供crdb_internal.node_transaction_statistics展示丰富的事务信息。TiDB Lock View 吸收了这些思路并针对自身TiDBSQL 层/ TiKV存储与锁管理分离的架构做了适配凡 TiDB 自身可知的数据运行中事务、死锁历史、事务摘要直接由 TiDB 侧内存表提供并支持CLUSTER_集群聚合凡 TiKV 持有而 TiDB 不可知的数据真实锁等待则通过新增 store 级 RPC 拉取并通过resource_group_tag打通锁/死锁事件 ↔ 具体 SQL的关联。从设计到落地这套体系的关键演进点新增可读 SQL 列、增加Idle状态、按 5000 上限校验事务摘要容量等都能在 pkg/infoschema/tables.go、pkg/config/config.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),仅供参考