![7b746b2c0d43bb1d9f4c02028a117978.png](https://i-blog.csdnimg.cn/blog_migrate/dd92935eaad1f32e46b817dd1e28308b.jpeg)
在前面文章中我们有介绍 CRDB 如何基于时间戳进行并发控制,并且也有看到 CRDB 通过 forward timestamp + commit refreshing 和 txnwait.Queue
等机制能在 Serialize 隔离级别下减少冲突,但 CRDB 中帮等待只是在部分场景下的“优化”,并不能避免在遇到冲突时 Retry,本文我将集中边学习边记录下 CRDB 的事务重试处理机制。
重试种类
其实对于所有数据库尤其是不使传统锁机制的的数据库希望避免客户端重试,都希望业务应用能"面向无脑编码"就能用上自己,而不是需要应用过多处理重试,进行"面向异常编码"(虽然处理异常是程序员的价值所在),所以目前多数类似数据库在需要重试时进行等待或自动重试减少客户端介入,然而不是所有的事务都可以自动重试,一般来说满足
Transaction's statements are not conditional on the results of previous statements
情况的事务,数据库可以帮助客户端自动完成重试,具体对于 CRDB 来说:
- 对于隐式事务单条语句执行: 如果没有结果集返回可以自动重试(i.e. 单条 insert);如果有结果集在第一条结果集合返回客户端之前可以自动重试,如果已经有返回则不可以重试,因为 CRDB 支持产生一定结果集(
sql.defaults.results.buffer.size
)后提前返回部分数据给客户端,这样的好处是可以减少 CRDB 自己自身需要 buffer 的内存,代价就是如果这条查询如果需要重试只能报错让客户端重新查询,如果自动重试无法保证新查的数据和之前部分返回的数据一致 - 对于 PG 协议支持的 Batch 或多个
;
连接的 Multi-Statement: 可以 CRDB 可以进行自动重试,因为只要能做 Batch 或 Multi, 则表明应用程序不可能有那种 if 查处结果 A then 更新1 or 更新2 的逻辑,数据库可以帮助客户端重拾 - 对于多条语句非 Batch 的情况:即大家熟悉的
begin; select1; update1; update2;... commit
的事务数据库在遇到冲突错误时则不能重试,只能让客户端回滚并重新发起事务; 主要问题如果自动重试其中查询或基于更新影响行的判断条件会发生变化为和之前应用程序所获得的信息不一致的样子(e.g. 应用代码是账户余额有 100 块以上就发个优惠券, 数据库只能自动重试"查询余额"和"插入优惠卷"的 SQL 不能重拾"if amout > 100" 的代码逻辑,所以需要客户端事务代码重试 ); CRDB 对于客户端重试还提供了一些高级 SQL 语法并提供了一些语言的客户端 Driver 来提高重试效率并简化客户端编码 - 除此之外有些错误在更低层次(比如请求维度)如果能直接重试(这也将是本文前半部分主要介绍的内容)
另外,重试机制高效与否会影响数据库在高冲突负载下的吞吐(比如: 秒杀,热点账户),对于 CRDB 来说越靠上层的重试越昂贵越靠下层的重试越轻快,下面我们从根据重试代价从低到高看下 CRDB 在各个层级进行的重试处理。
Replica 重试
一个客户端在 coordinator-side (接收 SQL 的节点)被转换为 key 维度的 BatchCommand 之后根据 range 分布被发送到对应 replica-side 进行处理,在 replica-side 被实际处理并进行冲突检测,所以可以想到的是重试如果能越来靠下层提前处理不用回到上层越好,比如本节介绍的错误可以在当前 replica-side 直接处理不用返回 coordinator-side 就进行重发可以避免大量的网络交互开销。
1)Batch Eval 部分重试 [in replica-side]
这里主要重试的是 WriteTooOldError, 在《CockroachDB Transaction 小记 - III(时间戳并发控制)》的那状态图中可以看到这个错误在事务是一个较老的时间戳向一个已经有更新时间戳的 Key 写数据时发生( WriteTooOldError 比较特殊在产生的同时还是会写一个比当前 Key 最新 Committed 时间戳更新的临时 Key 并返回错误,见这里)。
在发生 WriteTooOldError 后:
- 如果当前请求是 1PC 的请求,则需要看下当前请求是否需可以不做 refresh 也可以直接向后 forward 提交 timestamp(即当前事务根本没有需要去 refresh 的 span