请问乐观事务下如果Primary Key Prewrite失败会怎么样?

为提高效率,请提供以下信息,问题描述清晰能够更快得到解决:

【TiDB 版本】

【问题描述】

TiDB的代码逻辑是:如果Action是Prewrite,则以region为粒度并发的发起请求。并不需要像Commit时一样先处理Primary再处理Secondary。

我的疑惑是:当前事务为A,假设Primary Key所在的region Prewrite失败了,其余region都成功了(即Secondary Keys全都加锁成功)。
这时候SQL宕机,后来的事务B帮助清理残留的锁,B发现A的Primary Key没有Lock信息,按照Percolator原文的描述,此时B的判断是A成功提交了。实际上A没有成功,TiDB是怎么处理这个case的?是通过A的Primary Key上的时间戳么?


若提问为性能优化、故障排查类问题,请下载脚本运行。终端输出的打印结果,请务必全选并复制粘贴上传。

你的假设是错的,只有 Primary key 的加锁成功以后,才会执行 second keys 后续的动作(这个后续动作分为同步和异步两种处理模式),以下是官网的文档地址:
https://github.com/pingcap/docs-cn/blob/master/optimistic-transaction.md
https://github.com/pingcap/docs-cn/blob/master/pessimistic-transaction.md
https://github.com/pingcap/docs-cn/blob/master/transaction-isolation-levels.md
https://github.com/pingcap/docs-cn/blob/master/transaction-overview.md

Primary key 加锁,也有两种解释,需要根据你当前配置的事务模式来解释:

  • 乐观模式
    执行时不加锁,提交的时候才加锁,如果业务上干扰比较大,会有很多冲突(自带重试机制,可以关闭)
  • 悲观模式
    执行时就加锁,提交成功以后就释放,加锁后对于业务TPS的影响比较大,需要衡量和比较;(这个加锁如果失败,也有重试机制,会根据PSO去获取最新的数据,做为重试的依据;同样也可以关闭)

希望对你有所帮助!:grinning::nerd_face:

感谢回复!

但还是有些疑问。

  1. 你给的第一个文档 https://github.com/pingcap/docs-cn/blob/master/optimistic-transaction.md 乐观事务原理的第5步第iii点

TiDB 并发地向所有涉及的 TiKV 发起 prewrite 请求。TiKV 收到 prewrite 数据后,检查数据版本信息是否存在冲突或已过期。符合条件的数据会被加锁。

也就是说Prewrite是完全并发的,并不是“先对Primary加锁,成功了才对Secondary加锁” <====这也是我的困惑之处

而第5步第vi点

TiDB 向 Primary Key 所在 TiKV 发起第二阶段提交。TiKV 收到 commit 操作后,检查数据合法性,清理 prewrite 阶段留下的锁。

也就是说Commit是先处理Primary,再异步清理Secondary <=====我对这个流程没有疑问

  1. 浏览TiDB代码也确实是完全并发的。

     commit 682e49b64710cb931fbde6b668dbf9ca336e62a7 (origin/release-5.0)
     store/tikv/2pc.go
     func (c *twoPhaseCommitter) doActionOnGroupMutations
    

actionactionPrewrite时,直接就调用了doActionOnBatches,确实是完全并发的

并发请求和加锁的操作,是两步,并不冲突

  1. 文档中已经说明了,符合条件的数据才会被加锁
  2. 只有加锁的过的请求,才能到下一步执行
  3. primary 执行成功后,才会触发下一步

然后,并发和加锁也是两个概念,打个比方:
现在你买去买馒头,但是店铺只有一个收银的人,这个时候买单的小伙伴,是不是必须排队?
那么,只有一个人能够正确的买单,其他的人要么等待,要么离开(放弃)

这样描述,你能理解么?:nerd_face:

同一个region内的primary key和secondary keys做到你提的3点是很容易的,但是不同region之间是完全并发的,我还是困惑怎么“等待”呢?

假设这个事务涉及3个key,ABC,其中AB在region1,C在region2,A是Primary Key,BC都是Secondary Keys。
doActionOnBatches是按照region粒度并发的,也就是说region1和region2的请求是发到不同的tikv节点上并行处理的。那么region1在处理Prewrite请求时可以先处理A,成功了再处理B。
但region2在处理C的时候怎么做到“A成功才锁C”呢?

你的这个问题涉及的面就很大了,需要说到 PD了

不用假设,实际的情况比你说的要复杂的多,所以 PD 的存在就是为了解决这个问题的

比如,你要写一个数据,那么这个数据可能跨了很多 tikv,怎么感知呢? 通过PD 的调度,来获取到 TSO 和 元数据的。所有的定义都是PD 来共享的;

然后,你具体要写到哪个 TIKV,通过PD 的信息也可以得到了。

至于执行的过程,参考以上我的回答,希望对你有帮助! :nerd_face:

你具体要写到哪个 TIKV,通过PD 的信息也可以得到了

我对这个没有困惑,我的困惑只在于:

怎么做到你说的“只有 Primary key 的加锁成功以后,才会执行 second keys 后续的动作”。
在这个例子里,就是“只有A加锁成功以后,才会对C加锁”
你的意思是,region2在处理C的时候主动去region1查看A是否上锁了吗?

这个和锁无关了啊,A已经确认加锁的话,后续就触发执行了;
执行完毕以后,清理完成 A ( Primary 相关的执行信息的时候),
就会推送 C( Second 相关的处理事件),到相应的任务队列了,保证Second 处理一定成功

好好看下论文…
Peng.pdf (218.6 KB)

就是因为代码和论文不一致才有困惑……

论文里的第4页Figure 4里的example明确展示了是先对primary加锁(也就是bob),然后才对Secondary加锁(也就是Joe)。

但是我没有找到TiDB先对A加锁,再对C加锁的逻辑。你说的“任务队列”是哪部分代码呢?

换个表达方式吧

我和你的共识是“只有 Primary key 的加锁成功以后,才会执行 second keys 后续的动作”

但我没找到对应的代码逻辑,根据我看到的代码,TiDB的实现方式是并发地对不同region发起Prewrite请求。但我不知道为什么可以这么做。我觉得应该先给Primary Key所在的region发,成功了才给其余region发。

好好翻下源码,你能找到的

同问:

  1. Primary key 和second key 如果分布在不同的tikv节点,那么其他tikv 节点是如何感知到Primary key 写入成功的呢
  2. 在乐观事物模型下,tikv 是如何处理死锁的问题呢?(key 分布在多个tikv节点的场景)

问题1 已经回答了;
问题2 :
死锁,会进入到重试环节(看你是否开启了重试),也有可能会一直冲突(错误的业务模式)
这种情况建议用悲观锁;
另外一种,就关闭重试,会直接返回提交失败

: )我看了代码,TiDB的实现在region级别上确实是完全并行的,没有先等Primary加锁成功再给Secondary加锁的逻辑。

能这么做的原因是假设primary key lock失败了,也就是primary key上并没有锁信息,因此primary key上还是前一个事务的commit ts。那么secondary keys自然能根据时间戳判断出来事务是没提交的。

1 个赞

问题1 我还是不太理解上边的回答

  1. 文档中已经说明了,符合条件的数据才会被加锁
  2. 只有加锁的过的请求,才能到下一步执行
  3. primary 执行成功后,才会触发下一步

这里能够在详细介绍一下primary 加锁成功之后的操作步骤吗,尤其是primary 和 second 在不同的tikv 节点上的时候,多个存储节点是如何协同加锁的呢?

上面的回答中,有几个URL都描述了这个过程;
另外跟你说明下,加锁是逻辑上的,二阶段提交本身就保证了这个一致性的过程;

加锁是物理上的,需要写到RocksDB以及raft里

物理上锁?你确定么?:nerd_face:

理解不同罢了,重点是后面半句“需要写到RocksDB以及raft里”

你这个理解是正确的,primary key 失败了,secondary key 会回溯到 primary 上面,不会认为这是一个成功的提交。会把 secondary key 的锁清理