在上一篇《CockroachDB Transaction 小记(I) - 简单流程》,我们看了下 CRDB 基础的事务实现,本文我们会基本跟随的 CRDB 开发过程以学习为目的在之前介绍的事务模型基础上进一步看下这几个改动的设计以及代码实现,大概会包括前伏后继的 3 个优化:

这三个优化之后大家会发现都有一个类似的想法将 one local condition 转换为 distributed condition,来避免对在集中单点状态上的等待。

Lazy Transaction Record Creation

概述
在上一篇文章中出于简单易于理解,我们说的在“开始事务的时候 CRDB 就会先写 Transaction Record”,并让 Write-Intent 指向 Transaction Record,而在当前版本的 CRDB 中这是不准确的, CRDB 其实会努力避免或延迟创建 PENDING 状态(即 Intent 还未 abort 也没 commit 的中间状态)的 Transaction Record, 在未创建期间之前依赖 record 的功能会通过其他 intent 的 liveness 来 synthesize 出这个未写入但逻辑上存在的 Transaction Record 处理。
如前一篇文章(I)所述,在 CRDB 不是 OCC,在每次执行 DML 都会作为 Phase1 向存储写 WriteIntent,所以通常会在第一条 DML 执行写 WriteIntent 时顺带写 Transaction Record,提前写入的 Transaction Record 会让后面 Parallel commit 需要等 Transaction Record 的创建(parallel commit 希望的是直接写 stage 状态的 record,如果之前有就有 pending 状态的 record 需要等 pending 的 consensus 写先完成才能改 stage,这样 parallel 的效果会受限, 这部分可以看到后来回来看就能理解哈~),通过延迟创建,可以做到和最后一组 intent 一起写入 pending 下个状态的 record(commit or stage)的效果,进而支持 Parallel commit 的实现, 另外这个改动可以在很多时候少写一个 kv 减少对存储的写入压力(可能会觉得多写一个 kv 没什么但考虑有些高 qps 进行大量点更新的场景还是有些用)。
先看下 Transaction Record 的职责,在之前修改之前它会充当:

  • the single linearization point of a transaction: 通过记录上的状态识别这个事务是 committed 还是 aborted(后面 Parallel Commit 这个的 committed 判断被修改,这个小结影响不大)
  • truth for the liveness of a transaction: coordinator 会定期发送心跳到 transaction record 来解决“还没提交事务发起者就挂了”的问题,这个 liveness 在 lazy 后的处理需要改变
  • bookkeeping on the resources held by finalized: 通过 transaction record 上的信息才能完成对 commit 或 abort 后 intent 的 resolve(也就是 clean up 过程,这个小结影响不大)

在 Lazy 创建 Transaction Record 后,影响比较大的是第二个 the liveness of a transaction,通过上一篇文章我们知道,在两个事务发生冲突时会通过冲突的 WriteIntent 找到对应的 transaction record,并根据上面最后一次被心跳更新时间来决定事务的 liveness 是不是 abort,但现在 Transaction Record 没了(在第一次心跳前),所以当冲突事务 push 发现没有 transaction record 时需要转而通过看 intent 来进行判断,intent 的写入其实也是事务客户端在那个时间点的存活证据,而 CRDB 的事务模型中有事务心跳心跳会创建 intent,所以如果 intent 在 TxnLivenessThreshold 内则不存在可以认为这个 transaction 是 pending 按照 pending 进行等待处理,如果超过 TxnLivenessThreshold 则应该有心跳但实际还没创建可以认为事务已经挂了可当做 abort。
这里再罗嗦下事务心跳机制,目前 CRDB 心跳是发向 transaction record 且需要写和修改 transaction record 的,作者有提到有计划将心跳转向 coordinator 发这样可以这样就可以避免心跳写 pending 这步骤,并且理论上可以进一步简化目前每个 txn 维护 liveness 的复杂性,详情可以看这段注释
代码
回到代码, 首先是不会再在发送的 BatchCommand 中附加 BeginTransaction,因为不再需要一个单独的 begin 去存储节点创建 txn record,而是在需要时 synthesize 或新写入。
对于心跳处理 HeartbeatTxn 在发现没有 Transaction Record 时,不再直接报错而是写一个 pending 状态的 Transaction Record 记录,不过这里有些复杂的是需要区分出“真不存在”还是“存在过但被 abort 了且被 GC 掉了”,对于后者不能直接创建,所以会先做一组检查进入 CanCreateTxnRecord 这个方法, CanCreateTxnRecord 是整个功能的实现核心(在注释中作者解释了未来可以简化的思路),这里实现巧妙利用了之前提到的 timestamp cache,在上一篇中我们提到通过 timestamp cache 可以在 replica 上知道 key 或 key range 的读取或写入的最高水位,而这个在 timestamp cache 中的 key 也可以是 Transaction record key,通过 timestamp cache 可以对应 transaction record key 的水位我们可以进行如下判断:

  • 如果”写水位 ts < 当前事务 txn.minTs(第一个 intent laid down 的时间戳)” 则允许通过读水位 ts 来 synthesize txn-record
  • 否则则表明在 transaction record 上有过其他变更
    • 如果对应写水位的事务 id 是当前事务则表明请求可能是重复请求返回 abort
    • 如果没有没有对应 key 的事务 id 信息(uuid.Nil), 且 “tsCache 的 lowWater < 当前事务 txn.minTs”(上篇文章提到过在没有时 tsCache 返回的 maxWrite 是整个 cache 的 lowWater 且 lowWater 会在 lease holder 变化时设置),这种情况多是 leaseholder 变更或 cache 被清了也返回 abort 错误等客户端用新 tid 重试
    • 如果对应写水位的事务 id 是其他事务则表明其他写入在尝试通过 tsCache 来组织当期事务写入所以当前事务需要 abort 报错等客户端用新 tid 重试

来决定能否给 heartbeat 虚出一个 pending 的 transaction record 还是报 abort 错。
对于EndTransaction 处理和心跳类似通过 CanCreateTxnRecord 方法检查能否创建 Transaction Record 和 如果创建用什么被 push 过的 timestamp,只不过如果可以创建创建的不再是 Pending 而是 Commit(在能否创建 Transaction Record 和 如果创建用什么被 push 过的 timestamp,只不过如果可以创建创建的不再是 Pending 而是 Commit(在后面的 Parallel commit 后会是 Stage ),在不可以创建同样 abort 错误。
对于 PushTxn 对没找到 Transaction Record 的处理,同样通过 CanCreateTxnRecord 方法检查 synthesize 出来的 txn 应该是 pending 还是 abort 状态和如果能 pending 应该用哪个被 push 过的 timestamp 来做时间戳,如果是 pending 还会会通过 IsExpired 检查是否超过 TxnLivenessThreshold 应该 abort push。对于 PushTxn 有一种特殊情况是 pusher 希望将 pushee 的提交时间戳往后推迟,之前的做法是将 transaction record 上的时间进行修改,但 lazy 后 push 时没有 transaction record,目前代码实现是通过让 push_request 将 transaction record key 对应 read ts cache 水位后移,来达到类似来避免写 txn record 又让后后续的 synthesize 能被往后推的效果,代码见这里
最后还有 QueryTxn,即当事务冲突在在 txnwait queueu 中等待时会定期向 pusher 查询是不是已经已经自己 abort 的查询(可以参考《小计(i)》),目前实现是也是会从 meta 中 synthesize 一个 pending 或 abort 的 txn 过程和前面几个处理非常类似。
最后是对于 1PC 中“如何判断一个完整事务”条件从“有 begin 有 end”变化为对 seq 的校验,所以现在每个 cmd 都会有个 seq 来标明当前处理在 txn 中第几个。
小结
通过这个优化, CRDB 可以尽量避免在写 writeIntent 的时候创建 transaction record,理想情况可以直接等提交时创建 commit/rollback/stage 状态的 record,而在没有 record 这段时间里如果需要看状态则通过 intent 状态/心跳间隔/timestamp cache 来 synthesize 出一个虚拟的 transaction record,到这里开销主要是每次 DML 写 intent 的代价了。

Transaction Pipelining

概述
在解决了 Transaction 提前创建的问题后,对于 Phase1 最明显的问题就是不用 OCC (不用 OCC 是因为 CRDB 选择 serializable 且希望减少冲突 lock 更适合)导致的每次 DML 都要等 consensus 在多个节点完成才能执行下个语句到导致的 N 次 consensus 开销。这个 Transaction Pipelining 的目标是希望 Phase1 的开销不再随着语句数目 N 的增加而线性增长(虽然这个也可以通过将 DML batch,比如改写 insert values (…),(…) 来缓解; 另外其实 CRDB 还提供一种特殊语法 RETURNING NOTHING来让用户声明 DML 不需要结果来尝试 Parallel 的执行 N 个 DML,但要用户改成特殊 SQL 并不实用并且会让报错和影响行变得很混乱,目前有 pipeline returning 没什么用了)。
Pipelining 就是大家熟悉的“不等上一条结果返回就发送下一个请求”,对于当前的 SQL 协议其实并不能做 Pipelining 一个客户端到数据库的链接上正常只会有一个请求在 inflight,下一个 SQL 需要等上一个 SQL 的响应返回(包括错误和影响行)才能进一步发送下一个 SQL,在这个优化里的非常巧妙的是将 SQL 执行结果分解为:SQL 执行成功失败(比如:约束和影响行)和 Consensus 执行成功失败(Raft 同步并 apply Rocksdb),并只让后者做 Pipelining,即在 Begin 之后的 DML 会在 local 检查完成 SQL 约束和影响行后就返回给客户端让其继续发下一条语句不必等待这条 DML 修改在 consensus 中的执行成功,直到运行 Commit 的时候才去看之前写过的 intent “是不是都已经 consensus 完成了?”这个过程也叫 proven,如果没有完成会等待同步或 commit 报错(所以可以想到的是在 coordinator 是需要多记录下用过的所有 intent 最后提交的时候来“算账”的)。
通过这样一个过程将 Phase1 中的 DML 都变成只需要修改 range leaseholder 所在节点内存即可继续执行下一个语句的效果,不需要等 consensus 和写 rocksdb,如果当前收 SQL 的节点正好就是 range leaseholder 的话,可以说将 Phase1 等 consensus 从 N 次变成 0 次,但 Phase2 的 commit 代价因 pipelining 会从 1 次变成2次(一次等 intent 都 consensus 完成,一次修改 transaction record 为 commit), 在下一个 Parallel commit 则会进一步把 2 次变成 1次。
不过只要 local 检查 ok 就返回不等 consensus 看上去 ok, 但让问题变得复杂的从来都是一些细节场景,上一篇《CockroachDB Transaction 小记(I)》 中有描述“读取事务自己写的 WriteIntent”的处理,在 Transaction Pipelining 后,begin 第一条语句写入数据,第二条接着执行希望读取第一条的结果时因为没有 consensus 完成第一条就返回了所以在 MVCC 中会读不到这个 writeIntent,为了解决这个问题需要维护一个语句的依赖关系,对于第二条需要提前等 consensus 完成才能读;既然当前事务可能漏看自己的 writeIntent,其他事务也可能会漏看我自己写的 writeIntent 呢?答案是不会出现”漏看 writeIntent”, 不管有没有 pipeline 目前的 CRDB 能保证对于一个 key 有且只能有一条 writeIntent 记录存在,在我们之前文章中有提到 Latch Manager 会对在 lease holder 通过 latch 来 serializes 写操作,虽然 pipeline 后我们没有等 intent replicate 完成就返回给用户了,但 latch 会一直持有直到 replica 完成才会释放,所以对应 key 如果有其他事务尝试写 intent 会被 latch 挡住直到前一个操作完成,这样一定能看到别人的 writeIntent 并发现冲突。
代码
为了实现对 inflight 请求的维护并且在发生自己查询自己写的 intent 或 commit 时确保 inflight 的请求都已经 consensus 完成,功能的开发者将 TxnCoordSender 进行了重构,引入了 interceptor 的 pattern 来让 pipeline 和后续功能不至于让 TxnCoordSender 职责过于混乱。
而整个 pipline 处理的和行逻辑处理逻辑位于 txnPipeliner 这个 interceptor 中,他会对 batchCommand 请求进行 intercept, 主要围绕 txnPipeliner 结构体上的两个成员变量:

  • ifWrites: 维护所有 async 发出但还未 proven 的请求,具体是一个 key -> seq 的 btree,通过 seq(可以理解一个 txn 中的第几个 op) 后面结合当前 txn-id 信息就可以到 queryIntent
  • footprint:维护所包含所有有写过 intent 的 span(key range)信息,async 的请求在 inflight 被 proven 后会移动到 footprint,而同步或发送失败的请求会直接进入 footprint,这个 spanSet 主要是为了 commit 之后 resolve intent 时能确认一个范围批量 resolve,所以这个 span 会被 condense 来节省内存(即这个范围可以比实际更大一些但会包含所有需 resolve 的 intent,然后 scan 批量处理的时候通过 txnid 过滤下就好了)

目前的处理非常简洁位于 SendLocked,这里分三个部分说明:
1) 等待 inflight 和 标记 async
首先是在发送请求前 chainToInFlightWrites 会:

  • 对所有请求中和 ifWrites交迭(即有去 write 或 read 还在 inflight 中数据,也就是自己读写自己的依赖语句)的请求,在 Batch 的位置之前插入一个 QueryIntent 请求(会考虑去重),这样前面提到的有依赖的读写会先等 intent 能被 query 到即(能被 proven)才能执行;
  • 对于非事务中的请求会将当前所有 inflight 插入 queryIntent
  • 对于 comimit 的 EndTransactionRequest 同样会将所有 inflight插入 queryIntent 的 cmd, 来保证 commit 之前所有 intent 被 proven

而 storage 节点收到 QueryIntent 会查询 mvcc 来看能否找到对应 intent 数据,需要注意这个方法的, 如果找到且 intent 没有被 push 即代表 consensus 已完成 intent 已 proven, 如果被 push 会返回 RETRY_SERIALIZABLE 来先 refresh ; 如果没有找到因为 ErrorIfMissing 非测试代码都是 true h会返回 IntentMissingError,并在回到 txnPipeliner 后被转换为 Retry Error进行重试。所以通过预先加的 QueryIntent 达到了等 inflight 的请求先 proven 再执行的目的。
当前 queryIntent 发现 intent miss 时,会通过 timestampCache 让对应 key 写的 intent 只能发生在未来
另外这个方法中还会对不适合 pipeline 处理的情况不用 async 的情况进行判断:

之后对于能 async 的 request 会在请求上标识 AsyncConsensus, 之后在 storage 节点会看到标记后会对这个请求async 处理不等 proposal 完成就先返回
在 async 处理过程中如前面所属会一直持有 latch 来排序对同一个 intent , 代码中对 latch 引入 move semantics 感觉不错。
2) 发送请求后维护 inflight 和 footprint
在发送出请求后会进入 updateWriteTracking, 会处理请求结果,如果请求失败或事物已commit 或 abort 则将 inflight 清理移动到 footprint(commit 其实不会移), 如果是前面插入的 QueryIntent 的结果则从 inflignt 中移除并加入 footprint, 如果其他如果是 async 的请求则加入 inflight 等待后面的 proven, 如果是同步的则直接加入 footprint,最后退出这个方法前会对 footprint 做一次 condense 来节省维护 footprint 内存(范围包含就好没必要准确维护每个intent)。
最后 intercept 会将插入 queryIntent 的 response 给 strip 掉,其他 interceptor 或调用者不用关心这个新插入的 queryIntent。
3) 发送 commit 请求(EndTransactionReq) 的处理
最后在事务发送 EndTransactionReq 时会先进入 attachWritesToEndTxn, 会将当前 pipeliner 中还在 inflight 的 write 和 footprint,加上当前 batch 中的其他 write 合在一起放到 EndTransactionRequest 中(因为可能是最后一个 batch 即有 write 又有 endTxn 的情况), 这个会供后面 ParallelCommit 处理时使用。
对于 EndTransactionRequest, 如 1) 所述 会在 chainToInFlightWrites所有 inflight 加 queryIntent 来保证 commit 之前所有 intent 必须被 proven。
最后需要注意的是 EndTransactionRequest 这批写入是不会继续 pipeline 这个 Batch 会使用同步方式经过 raft consensus, 因为 EndTransactionRequest 不是 TransactionWrite 所以最后带 end 的一批会是同步过的 raft 也执行完成即表明我们前置插入的 queryIntent 已经满足且当前批已 replica。
小结
Pipeline 通过对请求 async 处理并维护 inflight 的 request, 在发送 commit 或依赖请求时插入 QueryIntent 来达到必须先 proven 再执行的效果。显而易见的是 inflight 不能太多不然会消耗很多内存维护 inflight 且在 commit 时需要大量 proven 处理,作者在注释中提到尝试用 3 种方式缓解,前两种都是提前用 queryIntent 轮寻去预先异步 proven 来减小 inflight 大小但问题是很多小事务这样会发一些费的 queryIntent,最后一种是可以通过 grpc streaming 来支持一个请求多个 response,这样可以先返回一次 response 相应用户之后等 async 处理完后再 push 一个 response 回发起节点提前 proven, 感觉最后这个想法感觉不错但代码里还没实现。

Parallel Commit

概述
通过 Transaction Pipelining 如果一切如自己所愿(没有语句前后依赖和冲突)则可以将总共 N + 1 次等 consensus 变为 2 次, Parallel Commit 的目标是让 2 次中的 Commit 操作也从导致用户等待的因素中消失。核心思路是:修改 commit 认定条件从“有 commit 状态的 txn record”变更为“有 commit 状态或 stage 状态但所有 promised writes 都已写入成功”,从而让 txn record 可以搭车最后一批 batch 并行写入,并在 intent 写入完成后即可返回客户端提交成功,然后异步做将 Stage 转 commit 状态的 consensus 修改,如果转换 commit 的发起者挂了,因为 txn record 中有所有 promised writes 其他事务在看到 stage 状态的 transaction record 时如果发现负责的事务已经挂了自己也可以通过 txn record 的信息来协助进行 commit 或 abort 完成恢复。
代码
让我们还是回到代码视角,首先从发起请求的 coordinator 开始看下
1) Normal Process
回到介绍 pipeline 时的 attachWritesToEndTxn,pipeline dml 后在执行 commit 语句时,有可能上一条语句的 dml 还没有被 proven 的,所以在发送 EndTransactionRequest 时会将 pipeliner interceptor 上所有 inflightWrite 和当前的 batch 中其他 write cmd 一起放到 EndTransactionRequest#InFlightWrites 中发送到存储层; 另外除了 inflight 的 write 也会将代表已经 proven write 的 interceptor 上的 footprint 作为 EndTransacationRequest#IntentSpans 向下发到存储层(主要为了能在提交后触发 resolve 流程), 另外这里再重复对于需要 proven 的 intent pipliner interceptor 同时也会添加 queryIntent cmd。
接下来这个被 pipliner interceptor 附加了 inflight 和 待 resolve span 信息的 request 会进入下一个 interceptor txnCommitter,在这个 interceptor 是 parallel commit 在 coordinator 端的核心处理逻辑,他主要拦截 EndTransactionRequest:

  • 首先,如果是 read-only(没有要 resolve 的 span 也没有 inflight 的 write) 但有 endTxn 的 batch 则去掉 endTxn 后直接发存储节点处理, 如果去掉 endTxn 后 变成空 batch 则直接诶省略发送 mock 一个结果向上返回,这部分逻辑在 sendLockedWithElidedEndTransaction
  • 之后和 pipeline 类似简单判断了能使用 parallel 的条件(集群版本,是否回滚,是否有 Trigger, 是否是点写), 如果不能 parallel 则清空 inflightWrite 提示存储不做 parallel 并补齐 intentSpan 并判断一手 span 的 distinct 方便存储节点优化处理。
  • 之后就将已经附着了 inflightWrite 和 intentSpan 的 EndTxnRequest 和其他请求做 batch 向下发送到存储节点
  • 在 committer interceptor 这还需要处理拦截处理存储层返回的响应结果,看如果 response 中的事务状态,如果直接就是 commit 则表明刚好 inflgith 的内容和 endTxn 落到一个 range 一起写成功; 如果是 stage 则表明进行了 parallel commit 写入成功(注意上一节有提到 endTxn 不会 async 过 consensus)即提交的 distributed condition 已经满足,可以先返回客户,然后再异步做一个转换 distributed condition 的 stage 状态到 one local condition 的 commit 状态的 makeTxnCommitExplicitAsync(其实就是又发了一个 endTxnReq 不过这次没有 inflightWrite)
  • 对于失败的情况如果 txnRecord 写成功了但别的失败了会对上层包装为 pending;另外如果已经写入 stage 状态的 txn record, 但整个 txn 的 timestamp 被 batch 中的其他请求给 push 到更后了,也需要报错触发重试

在经过 interceptor 最后进入 DistSender 后经过前面的处理 batch 需要进一步根据 range 做拆分发送到不同的存储节点,之前一篇 meta range 的文章中有提到过 divideAndSendBatchToRanges 这个方法,但在 parallel commit 后会,先在 divideAndSendParallelCommit 先做一次预拆分,目的是将 pre-commit 的 QueryIntent 单另出做一个 group 并向提交(在上节 pipeline dml 那在 commit 时我们在 commit 前添加了大量的 QueryIntent 来保证 inflight write 能在 commit 前被 proven,但 parallel 时写 stage txn record 可以和 proven 过程并行处理,不需要 pipeline stall 等 queryIntent,只要对上层保证都都返回成功才算成功即可分的 group 并向处理, 但对于非 pre-commit 自己读自己的 QueryIntent 不能诺出去还是需要 pipeline stall 保证正确性),预处理还是会用之前正常的 divideAndSendBatchToRanges 分 range 分发(这部分在 RFC 里也有提及)。
在 Coordinator 处理逻辑有些了解后,我们来看下存储节点的相关处理:
首先,是写入路径存储节点在收到当前 replica 的 batch 后会使用 maybeStripInFlightWrites 尝试将当前批次中的其他写入从 EndTxnReq 的 inflightWrites 中移出,因为这样些 write cmd 和 endTxnReq 会一起写入一起成功或一起失败; 有一种移除的极端情况是所有其他请求可以把 endTxnReq 的 inflightWrites 移空则可以优化处理跳过 stage(也正是前面 interceptor 那可直接看到 commit 而不是 stage 的原因)。
此外就是在 eval EndCommand 时会保存为 stage 状态,在响应中带 laid down 当时的 timestamp。
之后,回到 coordinator 如上面提到的会在 committer interceptor 等待刚发出的各种 QueryIntent(proven) 和写入请求完成并检查时间戳,提前返回用户,或重试事务。
2) Status Resolution Process
在 coordinator 没有挂掉时,会持续重试将 stage 状态的 txn record 和相关 intent 转换为正常 commit 状态或报错重试。但如果 coordinator 挂了,这些 stage 状态的 txn record 和相关 intent 会残留在哪,其他事务遇到后需要进行 Status Resolution Process。其他事务会在执行读或写亦或进行 gc 时如果遇到 intent 根据之前的介绍我们知道在会触发 PushTxnRequest 向 transaction record key 所在的 leaseholder 进入 txn wait queue 进行阻塞等待, 在等待中发现 txn record 如果是 stage 但被 abandoned(心跳没了),则开始触发 Status Resolution Process 通过检查 txn record 上的 promised writes 来决定是 commit 还是 abort,这个里需要注意的是 CRDB 目前有一个假设是只有发起提交的 coordinator 事务才会进行 commit 操作,对于其他事务去 status resolution 时会去看当前的 promised write 是否有已经提交的 fact 如果是则转换 stage 为 commit,如果当前有缺失的 write 不满足已提交的 fact 则会对事务进行 abort 并阻止这些可能迟到写入,所以这里还是守住了“只有事务拥有者才能将事务状态修改为 committed, 如果需要其他事务修改只会 abort 的设计”的哲学(这样按照作者注释文档的说发是为了简单)。
好了又看了一段理论我们来继续看代码,如上所述在 PushTxnRequest 在 txnwait Queue 等待 pushee 事务 abandon 后的 push 处理逻辑中如果发现是 pusher win 且 txn recode(可能是 synthesize 出来的)状态是 stage, 则通过返回 IndeterminateCommitError 来触发 recovery 过程(这里很自然利用了已经有的等待机制来在原事务“还活着”的情况尽量等原事务自己处理)。
之后在遇到 intent 的当前节点会开始处理这个 IndeterminateCommit 错误,处理过程同样会用 single-flight 来去重处理,之后的 resolution 分为 2 个 phase:
a. probe phase
首先会将 txn 中所有还在 inflight 的 writes,构建 QueyIntent 请求, 这个 request 在 pipelining 介绍中我们已经看到过,因为到这一步可以知道原事务已经挂了,所以这里只会去通过 QueryIntent 去 probe 看在当前时刻是不是所有的 inflight 都已经 laid down 满足隐式提交条件或有任意一个没有 laid down 那直接让事务 abort 并通过 QueryIntent 修改 ts cache 水位 prevent 对应时间戳之前的写,处理过程就是看 QueryIntent 的返回(QueryIntent 的 storage 处理见本文前面 pipeling 部分)。
同时还会定期在 requestBatch 中混插 QueryTxn 请求,来在 probe 过程中(这个过程可能并不快)观察 txn record 是否有被其他并发的变化, 比如被 commit 或 abort 那就不需要做 recovery phase。
b. recovery phase
在知道需要被恢复的 txn 应该是隐式提交还是被 prevent + abort 后开始 recovery, 会从遇到 intent 的节点向 txn record 所在 range 发送 RecoverTxnRequest 请求
对应节点收到请求后会先读取当前 txn record,这里可能有种极端情况是被 GC 了这时候会用前面用过的 SynthesizeTxnFromMeta 虚出一个不过这个方法一定返回 abort 状态的 txn(因 probe 过程已经用到 不过这个方法一定返回 abort 的(因 probe 过程已经用到 txn record 这里缺看不到只有 GC 一种可能)。
之后就是根据是 commit 还是 abort 检查状态和重试 epoch, 设置 commit 或 abort 状态,并将 inflight 清空合并到 intentSpans 中等待后面的 resolve,最后写入 writeBatch 然后过 raft 写入存储中。
最后在遇到 intent 的节点收到 recoverTxnResponse 且无错误则重试当前 command 即可。
小结
Parallel Commit 通过将引入 stage 状态并将未 proven 的 inflight 列在 txn record 中来达到类似一次 conensus 就返回客户端的效果,Resolution 过程正常会被事务发起者自己完成,其他事务会利用 txnwait Queue 来等发起事务,如果发起事务挂了(没 heartbeat) 正好会唤醒等 intent 的其他事务根据 stage 状态和 txn record 的 infilght 信息开始 recover,recover 实现主要通过 queryIntent 来确认是否都写完,是则提交,否则直接 abort 且通过 tsCache 来打住可能后到达的写入。?除此之外作者有提到未来可能会让 resolve 和 commit 也一起执行?

总结

本文我们继续看了下 CRDB 最近几个有意思事务修改实现,自己水平有限很可能看错有兴趣的同学欢迎讨论或让我再去看看~另外我自己也会继续看这块特别对于重试处理和 push 细节如果有新发现会更新内容。