2PC failed commit key after primary key committed

[2025/12/17 05:37:19.018 +00:00] [ERROR] [commit.go:200] [“2PC failed commit key after primary key committed”] [error=“Error(Txn(Error(Mvcc(Error(TxnLockNotFound { start_ts: TimeStamp(462933153015136331), commit_ts: TimeStamp(462933153906426548), key: [116, 128, 0, 0, 0, 0, 0, 0, 208, 95, 114, 128, 21, 84, 27, 34, 220, 94, 231] })))))”] [txnStartTS=462933153015136331] [commitTS=462933153906426548] [keys=“[7480000000000000d05f728015541b22dc5ee7]”]

现象就是写入的数据没查到。至于这个事务有没有写成功,不确定,程序没抓到报错。
我理解primary key 提交了就代表事务提交了。

有没有什么参数可以调整,让两阶段都提交后再返回成功。

或者说我这是遇到了已知bug吗?

我看 client-go 里面有相关的 issue :
https://github.com/tikv/client-go/issues/1631
是想加更多的信息,那么意味着你们遇到过?怎么解决?怎么避免?

这套是 rawkv 吗?

  1. TiDB 2PC 事务流程特性 :TiDB 的 2PC 事务提交流程中,在 primary key 所在 region 的 commit 成功后,会 异步并发 向剩余 region 发起 commit 请求。这意味着 primary 提交成功仅代表事务决议完成,但 secondary keys 的 commit 是异步执行的,若 secondary commit 失败(如 LockNotExist 等错误),可能导致部分数据未持久化,从而出现“写入的数据没查到”的现象。

  2. 已知 bug 及规避方案 :你遇到的问题可能与 async-commit 模式下的 PessimiticLockNotFound 错误有关——在 async-commit 场景下,prewrite 请求若遇到锁未找到等错误,会触发 tidb-server 返回 ResultUndetermined 错误,导致事务断链、数据未正确写入。解决方案是 关闭 async-commit (set global tidb_enable_async_commit = OFF ),切换为纯 2PC 模式:2PC 下仅当 primary key 出现 PessimiticLockNotFound 时才会导致 ResultUndetermined,能大幅降低断链概率,确保数据写入的一致性。

  3. client-go 问题的参考解决思路 :参考资料中提到类似的 client-go 事务重试内存泄漏问题(client-go#1451),解决方案是升级到修复版本(如 v8.4.0)。推测你提到的 client-go#1631 也可通过 升级 client-go 或 TiDB 至包含该 issue 修复的版本 解决。

  4. 强制同步提交的参数调整 :若需确保两阶段(primary+secondary)均提交成功后再返回结果,可关闭 async-commit 和 1PC(set global tidb_enable_async_commit = 0; set global tidb_enable_1pc = 0 ),强制使用同步的 2PC 流程。但此操作会牺牲部分性能,需根据业务对性能的容忍度权衡。

1 个赞

这是 tidb-server 报的错。

另外补充一下:这个报错如果返回给了客户端,客户端常用的访问 mysql 的那一套靠 durid 的封装没有抓到这个报错(也可能是我们程序写的挫),这会给客户端造成数据已成功写入的假象,建议这个错误至少能做个兼容处理,让客户端能感知到这个错误。

还有另一个错误客户端捕获不到:当事务超大,返回 out of memory 的时候,链接也没断开,我们一个程序会认为结果还没返回,就一直傻等着。这个问题不确定是程序导致的还是 tidb 自创的这些报错 jdbc 驱动无法拿到。

我理解primary key 提交了就代表事务提交了,这个没错。我也想知道如果远程节点在写入成功之前宕机的话,lock cf和write cf是在什么时候补偿的?


这里是把 <1,Jack>,<2,Candy> 两个属于不同region、不同tikv的key按事务写入tikv。

  1. 首先是prewrite,在default cf 和 lock cf 写入数据,把要写入的变更按region分成2个grpc,分别发送给2个tikv。
  2. 两个tikv 分别在default cf中写入数据,lock cf 中写入锁。其中 tikv1 中的是主锁,tikv2的是从锁
  3. 提交时,tikv_client 首先给主锁所在的region发送grpc,在write cf中写入commit记录put<1_100,100>,删除lock cf 中的锁<1,(D,pk,1,100…)>,这两个cf的数据利用rocksdb支持的多cf的原子写入保证同时成功或失败。
  4. 主锁提交成功后才给tikv2发送grpc,tikv2 在write cf中写入commit记录 put<2_110,100>,删除lock cf中的锁<2,(D,@1, 2,100…)>。 同样也是原子写入。

这里第四步,tikv2 的 grpc 是肯定能成功的,如果这时候 tikv2 的 region 不可访问了,等 3 副本选出新的 leader 以后再写 write cf 和 lock cf,如果3副本中2副本坏了,那后续恢复后,其他事务看到2 candy 上有没提交的事务,去检查主锁 1 jack已经提交了,就直接把 2 candy 提交了。也就意味着不可能出现提交失败的情况。都持久化了。

这里应该是碰到了 bug