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:运行中事务一览
该表描述当前正在执行的事务。设计文档定义的核心字段如下:
| Field | Type | Comment |
|---|---|---|
TRX_ID | unsigned bigint | 事务 ID(即 start ts) |
TRX_STARTED | time | 事务起始时间(人类可读) |
CURRENT_SQL_DIGEST | varchar(64) | 当前正在执行的 SQL 语句的 digest |
ALL_SQL_DIGESTS | text | 事务已执行过的所有 SQL 语句 digest 列表 |
STATE | enum('Running','Lock waiting','Committing','RollingBack') | 事务所处状态 |
WAITING_START_TIME | time | 当前锁等待开始后经过的时间(若有等待) |
SCOPE | enum('Global','Local') | 事务作用域 |
ISOLATION_LEVEL | enum('REPEATABLE-READ','READ-COMMITTED') | 隔离级别 |
AUTOCOMMIT | bool | 是否自动提交 |
SESSION_ID | unsigned bigint | 所属会话 ID |
USER | varchar | 用户名 |
DB | varchar | 数据库名 |
SET_COUNT | int | 当前事务修改的 key 数 |
LOCKED_COUNT | int | 当前事务加锁的 key 数 |
MEM_BUFFER_KEYS | int | 事务 membuffer 中的条目数 |
MEM_BUFFER_BYTES | int | 事务 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状态,且锁等待写作LockWaiting;ALL_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 侧,本身即是全局的:
| Field | Type | Comment |
|---|---|---|
KEY | varchar | 正在等待的 key |
TRX_ID | unsigned bigint | 当前正在等待锁的事务 |
SQL_DIGEST | varchar(64) | 正在尝试获取锁的 SQL 的 digest |
CURRENT_HOLDING_TRX_ID | unsigned 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_INFO(key 的描述信息,便于可读)与SQL_DIGEST_TEXT(等待 SQL 的规范化文本)两列。
3.3(CLUSTER_)DEADLOCKS:死锁事件历史
与上述两张"实时快照"表不同,(CLUSTER_)DEADLOCKS保存的是历史死锁事件,一个事件可能占用多行:
| Field | Type | Comment |
|---|---|---|
DEADLOCK_ID | int | 单个死锁事件需要多行共同表示,该字段用于区分不同事件 |
OCCUR_TIME | time | 死锁发生的物理时间 |
RETRYABLE | bool | 该死锁是否可重试(TiDB 会尝试判断当前语句是否(间接)在等待一个由当前语句自己加的锁) |
TRY_LOCK_TRX_ID | unsigned bigint | 正在尝试获取锁的事务 ID(start ts) |
CURRENT_SQL_DIGEST | text | 被阻塞的 SQL |
KEY | varchar | 正被死锁事件中另一事务持有、导致无法加上的锁 key |
ALL_SQL_DIGESTS | text | 该事务已执行 SQL 的 digest 列表 |
TRX_HOLDING_LOCK | unsigned 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_SUMMARY:
| Field | Type | Comment |
|---|---|---|
DIGEST | varchar(16) | 事务的 digest,由ALL_SQL_DIGEST计算得到 |
ALL_SQL_DIGEST | text | 该类型事务执行过的所有 SQL digest 组成的 json 数组 |
- 行生命周期:第一笔同类型事务结束后创建;缓冲区满后按LRU(最近最少使用)淘汰(依据
TRANSACTION_ID_DIGEST中的存在时间)。
(CLUSTER_)TRANSACTION_ID_DIGEST:
| Field | Type | Comment |
|---|---|---|
DIGEST | varchar(16) | 事务 digest(由ALL_SQL_DIGEST计算) |
TRX_ID | bigint | 事务 ID(即 start ts) |
- 行生命周期:事务结束后满足特定条件才创建(受内存限制),当前条件为"该事务执行过慢、更可能与其它事务冲突";缓冲区满后按 FIFO 淘汰最旧记录。
两张表的数据都可在 TiDB 侧收集:事务结束时把信息写入即可。作为旁证,当前仓库中该能力以TRX_SUMMARY表的形式落地:常量TableTrxSummary = "TRX_SUMMARY"及其集群版本CLUSTER_TRX_SUMMARY定义于 pkg/infoschema/tables.go 与 pkg/infoschema/cluster.go,对应列结构为tableTrxSummaryCols(DIGEST+ 全部 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 = ...; // 完整等待链 }kvrpcpb(KV 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 级 RPC
GetLockWait:用于从 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 吸收了这些思路并针对自身TiDB(SQL 层)/ 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),仅供参考