news 2026/9/11 5:55:37

时间戳排序并发控制:从原理到工程落地的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
时间戳排序并发控制:从原理到工程落地的完整指南

每个跑过并发事务的工程师,大概都经历过这种时刻:线上一个简单的"先查后写"操作,在低峰期一切正常,一到业务高峰就频繁超时、报错、数据对不上。排查到最后,十有八九是并发控制没做好。数据库领域的并发控制方案,最著名的两条路线是封锁(Locking)和时间戳排序(Timestamp Ordering,简称TO)。前者被传统关系型数据库用了几十年,后者则在不少分布式数据库和内存数据库里被发扬光大。这篇文章我想认真聊一聊后者——时间戳排序并发控制,把它的原理、实现细节、工程落地里的坑,以及和传统封锁方案的对比,一次讲透。

如果你是刚接触数据库内核的开发者,或者你正在做中间件、自研存储引擎,又或者你只是好奇"乐观并发控制到底是怎么做到不锁也能保证一致性的",这篇文章应该都能给你一份比较完整的参考。我会从最基础的冲突场景讲起,一直说到工业级实现里常见的时间戳分配方案和优化变种。

1. 从数据库的冲突现场说起:TO协议要解决的根本问题

1.1 一个并发更新的"车祸现场"

假设有一个账户表,两个事务同时在做转账操作。事务T1把A账户余额从1000改成900,事务T2把A账户余额从1000改成800。如果两个事务真的同时执行且没有任何干预,最终余额可能是900,也可能是800,取决于谁后写。但更麻烦的是,如果T1还读了B账户,T2还写了B账户,那最终的数据状态就完全不可控了——可能T1读到的是T2写了一半的B账户余额。

这个问题的本质是:多个事务并发执行时,它们的读写操作之间存在冲突,而系统需要保证这些并发事务的执行结果,等价于它们按某个串行顺序执行的结果。这就是数据库理论里的可串行化(Serializability)要求。满足可串行化,事务之间就不会互相干扰,数据一致性就有了底线。

传统做法是加锁:谁先拿到锁谁先操作,其他人等着。但加锁带来的问题也很明显——锁等待、死锁检测、锁升级,这些机制本身就有不小的开销和复杂度。时间戳排序走的是另一条路:不排队,不等待,用"先来后到"的规则直接决定谁有资格执行。

1.2 可串行化与冲突等价:TO的理论地基

时间戳排序协议的核心逻辑其实就一句话:给每个事务分配一个全局唯一递增的时间戳,时间戳越小的事务,优先级越高,越先执行。所有冲突操作必须按照时间戳的顺序来判定——如果低优先级事务的操作和高优先级事务的冲突操作撞上了,低优先级的事务要么回滚,要么被抛弃。

这里有个关键概念叫冲突等价(Conflict Equivalence)。两个事务T1和T2,如果它们交换了某些操作的执行顺序会产生不同的结果,那这些操作就是冲突的。TO协议的目标,是让所有冲突操作都按照事务时间戳的大小顺序执行,这样整个并发调度的结果就等价于按时间戳串行执行的结果。

注意,TO协议不是不要锁,而是原则上不加锁(某些实现为了一些内部保护可能仍会用到短临界区)。它靠的是"碰上了就判断谁该让路"的规则,所以理论上不会出现死锁——因为事务不会持有资源等待别的资源,每个操作要么立刻执行,要么立刻被拒绝。

这也引出了TO协议最鲜明的两个特征:第一,没有等待,就没有死锁;第二,冲突严重时会有大量回滚,性能可能断崖式下跌。这两点我们在后面的章节里详细展开。

2. 时间戳排序的两条铁律:读规则与写规则

2.1 时间戳从哪来:TS(Ti)的含义与来源

在讲规则之前,先把符号统一一下。我们用TS(Ti)表示事务Ti的时间戳,用W-timestamp(Q)表示数据项Q最近一次被写入时对应事务的时间戳,用R-timestamp(Q)表示Q最近一次被读取时对应事务的时间戳。每个数据项上都挂了这两个时间戳,它们记录了这个数据项"最后一次被谁写过"和"最后一次被谁读过"。

TS(Ti)怎么产生,这个看似简单的问题,其实在工程实现里有大学问。单机环境下,一个原子递增的计数器就够了。但在分布式环境下,如何让每个节点产生的时间戳既唯一又大致有序,是一个非常棘手的问题。系统时钟可能漂移,网络有延迟,每台机器的"当前时间"并不一致。这个问题我在第3节专门展开,先记住一点:TO协议的正确性完全建立在时间戳的全局有序性上,时间戳一旦乱序,后面所有规则都是空中楼阁。

2.2 读规则:晚到的事务不能读旧数据

TO协议的读规则如下:

如果事务Ti想读取数据项Q,且TS(Ti) < W-timestamp(Q),说明Q的最新值是由一个比Ti更晚的事务写入的。也就是说,Ti作为一个"较早"的事务,却想读一个"较新"的值,这违背了时间戳顺序。此时Ti必须回滚。

反之,如果TS(Ti) >= W-timestamp(Q),说明Q的最新值是由一个不比Ti晚的事务写的,或者就是Ti自己写的,那Ti就可以安全地读取Q。读取完成后,需要把R-timestamp(Q)更新为max(R-timestamp(Q), TS(Ti))。

这条规则背后的直觉是:既然T2的时间戳比T1大,在串行顺序里T2应该排在T1后面。那么T1在读数据时,绝对不能读到T2写的值——因为序列化的顺序是T1先执行,T1读到的应该是T2执行之前的状态。如果T1读到了T2写的值,说明实际执行顺序是"T2写→T1读",这和串行顺序"T1在T2之前"矛盾,整个调度就不可串行化了。

2.3 写规则:晚到的事务不能覆盖新数据

TO协议的写规则分两条:

如果事务Ti想写入数据项Q,且TS(Ti) < R-timestamp(Q),说明有一个时间戳比Ti大的事务Tj已经读过Q的最新值了。如果允许Ti写入,Tj读到的值就会被覆盖,Tj等于读到了一个"从未存在过"的旧值,这同样破坏可串行化。此时Ti必须回滚。

如果事务Ti想写入数据项Q,且TS(Ti) < W-timestamp(Q),说明Q已经被一个时间戳比Ti大的事务写过了。此时如果Ti直接覆盖,就相当于在串行顺序里排在后面的Tj把先执行的事务Ti的值覆盖了,也违反规则。同样,Ti必须回滚。

只有TS(Ti) >= R-timestamp(Q) 且 TS(Ti) >= W-timestamp(Q) 时,Ti才能成功写入。写入后,W-timestamp(Q)更新为TS(Ti)。

2.4 一张判定表搞定全部场景

把两条规则合并,可以整理成一张非常直观的判定表:

场景条件判定结果原因
Ti读Q,Q的最新写者更晚TS(Ti) < W-timestamp(Q)回滚TiTi读到了未来事务写的值
Ti读Q,Q的最新写者不晚于TiTS(Ti) >= W-timestamp(Q)允许读,更新R-timestamp符合串行顺序
Ti写Q,有更晚的读者TS(Ti) < R-timestamp(Q)回滚Ti更晚事务读到的值会被破坏
Ti写Q,有更晚的写者TS(Ti) < W-timestamp(Q)回滚Ti更晚事务写入的值会被覆盖
Ti写Q,没有更晚的读/写者其余情况允许写,更新W-timestamp符合串行顺序

这张表我建议想深入理解TO协议的朋友自己手推一遍。不要死记规则,而是思考一个问题:如果我不按这个规则走,最终会有什么后果?把每个违规场景都想象成一个具体的并发案例,规则自然就记住了。

3. 时间戳分配机制:为什么不能用gettimeofday()

3.1 单机物理时钟的问题:并发度一高就撞车

很多人第一次实现TO协议时,会想当然地用系统时间作为时间戳——反正时间戳嘛,当前时间微秒级,总该是唯一的吧?但实际一测就翻车了。单机上,两个线程在同一微秒内各自开启一个事务,拿到的系统时间完全一样。如果直接拿这个时间戳去做读写规则判定,两个事务就有一样的时间戳,谁先谁后就变成未定义行为了。

更隐蔽的问题是,系统时间可能回拨。NTP校时、手动改时间、虚拟机快照恢复,都可能导致系统时间倒退。一旦时间回拨,新事务可能拿到比老事务更小的时间戳,整个TO协议的判定逻辑就乱了——旧事务变成"未来事务",新事务变成"过去事务",数据一致性瞬间崩塌。

所以在单机实现里,最稳妥的做法是用一个原子自增计数器,事务启动时从计数器取一个递增值。这个方案简单可靠,但有一个瓶颈:所有事务的时间戳都要从这个全局计数器拿,高并发场景下这一个点会成为性能热点。实际工程里通常用批量预分配的方式优化——每个线程/核心预取一段时间戳区间,用完了再取下一段,把全局竞争从"每次取号"降为"每次取一段"。

3.2 逻辑时钟:Lamport方案的工程简化

单机自增计数器的问题在于分布式场景下不管用了——每台机器都维护一个自增计数器,两个节点产生的时间戳必然冲突。这时需要引入逻辑时钟的概念。

Lamport在1978年那篇著名的论文里提出了happens-before关系:如果进程A给进程B发了消息,那么A的某个事件happens-before B的某个事件,A的逻辑时钟必须小于B的逻辑时钟。在TO协议的工程实现里,逻辑时钟被简化成了这样一个规则:

  • 每个节点维护一个本地逻辑时钟,初始为0。
  • 本地事务启动时,本地时钟加1,作为事务时间戳。
  • 节点间通信时,消息里带上发送方的逻辑时钟值。
  • 接收方收到消息后,把自己的逻辑时钟更新为 max(本地时钟, 消息携带的时钟) + 1。

这样就能保证:如果事务T1在事务T2开始之前,并且T1的节点给T2的节点发过消息(即T1 happens-before T2),那T1的时间戳一定小于T2的时间戳。TO协议其实只要求冲突事务之间时间戳有序,而两个事务如果在不同节点上且没有通信,它们之间就没有先后依赖,谁先谁后都不影响串行化结果。这正是逻辑时钟能work的关键——它不需要全局时钟完全统一,只需要捕获真实的依赖顺序。

3.3 分布式系统里的混合时钟:HLC的思路

逻辑时钟解决了唯一性和顺序性问题,但它有一个缺点:时间戳和真实时间完全没有关系。这在业务上可能很麻烦——运维想看"这个事务是什么时候发生的"完全看不出,审计也做不了。

工业界的主流做法是混合逻辑时钟(Hybrid Logical Clock,HLC)。思路很简单:时间戳的高位取物理时间,低位取逻辑计数器。具体实现是每个节点维护一个HLC,规则如下:

  • 本地事件发生时,如果当前物理时间大于HLC的物理部分,则把HLC的物理部分更新为当前物理时间,逻辑部分归零;
  • 否则,HLC的物理部分保持不变,逻辑部分加1。
  • 收到消息时,取 本地HLC 和 消息携带的HLC 中的较大值,再按照上面的规则修正。

HLC的精妙之处在于,它在绝大多数情况下等于物理时间,只有在同一物理时间戳内发生大量事件时才用逻辑部分扩展。这样既保证了时间戳单调递增,又保留了和物理时间的对应关系。CockroachDB的时间戳方案就是HLC的工程化应用,后面第8节会再提到。

我在实际项目里的建议是:单机场景无脑用原子计数器或批量预分配,别再想物理时间的事;分布式场景优先考虑HLC,除非你的场景有非常强的"必须接近物理时间"的审计需求。纯逻辑时钟虽然简单,但排查问题的时候你会很想哭——日志里两个时间戳只差1,你完全不知道它们对应的真实时间。

4. 与2PL的正面对比:TO到底赢在哪、输在哪

4.1 2PL的核心思想与封锁代价

要对TO有深刻的理解,必须把它放进和传统两阶段封锁(2PL)的对比里看。2PL的思路是"冲突就在入口拦住":事务在读取或写入数据前必须先申请对应的共享锁或排他锁,拿到锁才能访问数据;所有加锁操作分为增长阶段和收缩阶段,一旦开始释放锁就不能再加新锁,以此保证事务的访问模式是可串行化的。

2PL在传统关系型数据库里统治了几十年,是因为它在读写比例均衡、并发冲突可控的场景下确实可靠。但它有两个绕不开的问题。

第一个是死锁。两个事务各自持有对方需要的锁,谁也无法继续,只能靠死锁检测或超时机制来打断。死锁检测本身要构建等锁图,成本和复杂度都不低。第二个是锁等待带来的尾部延迟。一个事务可能在锁队列里等很久,尤其是有长事务时,后面的短事务全部被堵住,响应时间像坐过山车。

4.2 TO的赢面:无死锁、无阻塞

TO协议在理论上的最大优势,就是没有等待。每个操作做判定时,只有"执行"和"回滚"两个选项,只要条件满足就立刻执行,不满足就立刻让事务失败。没有锁队列,就没有死锁,也没有锁等待导致的延迟波动。

这对某些场景来说是决定性的。最典型的是内存数据库和主内存OLTP系统。这些系统的核心卖点就是低延迟高吞吐,如果事务动不动阻塞在锁上,性能优势就没了。而且内存数据库通常不支持阻塞式锁等待的高昂代价——内存里数据多,锁粒度细,锁管理器的开销会被放大。TO协议用"冲突就回滚"换来了无阻塞的清爽,而且内存数据库的事务通常短小,回滚成本低,回滚了快速重试一次往往就成功了。

4.3 TO的输面:级联回滚与低并发下的性能损耗

TO协议当然不是银弹。它最大的代价是回滚可能级联。假设T1读了数据A,T2写了A并提交,之后T1尝试写B时因为和T3冲突被判定回滚。T1回滚后,所有读过T1写入值的事务——即使它们已经执行了很多操作——都必须一并回滚。这个回滚链可能很长,在极端场景下甚至可能引发连锁反应,导致系统整体吞吐瞬间崩塌。

这是TO协议最需要工程设计去缓解的地方。我在后面讲MVTO(多版本时间戳排序)时会提到,多版本化是缓解级联回滚的利器。

TO的另一个问题是在低并发冲突场景下仍然要付出时间戳判定的额外开销。每次读写都要比较时间戳、更新R/W-timestamp,这些操作虽然比加锁轻,但也是实实在在的开销。如果一个系统大多数事务互不冲突,2PL的锁开销可能比TO的判定开销更低,因为读写锁在无竞争时可以用非常轻量的原子操作完成。TO适合"冲突率较高但事务短小"的场景,2PL适合"冲突率低或事务较长"的场景。

4.4 到底该怎么选:场景判断经验

根据自己的经验,我总结了一套选择倾向:

  • 事务短小、冲突率高、对延迟抖动敏感的场景,优先考虑TO或MVTO。典型例子是内存数据库、金融转账类高竞争热点账户操作。
  • 事务长、读写混合、冲突率低的场景,优先考虑2PL或MVCC(多版本并发控制)。典型例子是传统OLTP业务,报表、订单、用户中心的常规读写。
  • 读写比例极不均匀、读多写少的场景,根本不应该用基础TO,应该用MVCC或多版本TO,否则读事务会被写事务疯狂误伤。

5. 工程落地中一定会遇到的几个坑

5.1 读多写少场景下,读规则误伤短事务

TO协议里,只要Ti的时间戳小于Q的W-timestamp,Ti的读就会被拒绝。在"一个热数据反复被更新,大量新事务同时读它"的场景下,新启动的事务时间戳都很小,但热数据的W-timestamp被频繁更新到很大的值。结果就是几乎所有读操作都会触发回滚,系统吞吐直接归零。

这就是为什么工业界几乎不会在纯读多写少场景单独用基础TO。解决方案通常是多版本化——读操作不再被W-timestamp拒绝,而是去读自己时间戳之前的最新版本。这个方法在MVCC里已经非常成熟了,PostgreSQL、InnoDB的MVCC本质上都在做类似的事。

5.2 长事务拖死短事务

TO协议对长事务极不友好。一个执行了很长时间、时间戳很小的事务,因为它"资格老",所有和它冲突的新操作都会被拒绝——但反过来说,它一旦需要回滚,级联范围也可能非常大。更麻烦的是,长事务持有的资源和它造成的回滚风险随执行时间增长,短事务则不断因为"撞上老资格事务"而回滚重试,整体表现就是系统越来越慢,长事务还迟迟不结束。

工业界对长事务的通行做法是:限制事务执行时间或操作数,超过阈值则强制回滚。这在设计时就该考虑——如果你的业务场景天然存在大事务,基础TO几乎注定要出问题,要么换MVCC,要么在应用层拆事务。

5.3 事务读不到自己刚写入的值

TO协议一个特别容易踩的坑是事务内先写后读。T1写入数据Q,Q的W-timestamp被更新为TS(T1)。随后T1又读Q,按读规则需要TS(T1) >= W-timestamp(Q)。这两个值是相等的,所以判定可以通过,不会出问题。但问题是有些工程实现为了性能,读路径没有检查"这个值是不是我自己写的",而是直接找了最新的已提交版本或最新版本,结果读到了自己提交前的旧值,或者别的并发事务写的值。这是一种非常隐蔽的逻辑错误,测试时很难发现。

我的建议是:实现TO协议时,一定要在数据项上记录"最后写入者的事务ID",读路径先检查写入者是否为当前事务,如果是就直接返回,不参与时间戳判定。这个判断成本很低,但能避免一大类疑难Bug。

5.4 版本号更新的原子性问题

还有一个容易被忽略的工程细节:R-timestamp和W-timestamp的更新必须保证原子性。多个事务并发读写同一个数据项时,时间戳字段可能被同时修改。如果这两个字段的更新不是原子的,可能出现一个事务读取后,另一个事务把它要更新的R-timestamp覆盖了,导致漏判冲突。

在实现时,我建议把"检查时间戳条件 + 更新对应时间戳"做成一个原子操作。单机上用CAS(比较并交换)就够了,不要图省事拆成两步。分布式环境下,这个问题会更复杂,通常需要借助数据项所在节点的本地锁或原子指令来保证。

6. Thomas写规则:给TO打上的关键补丁

6.1 Thomas写规则到底是什么

前面讲的写规则里,如果TS(Ti) < W-timestamp(Q),Ti必须回滚。但有一个著名的优化,叫Thomas写规则(Thomas Write Rule),可以显著减少这类回滚。

Thomas写规则的判定是这样的:

  • 如果TS(Ti) < R-timestamp(Q),说明有更晚的事务读过Q,Ti的写入会造成读到的值被覆盖,必须回滚。
  • 如果TS(Ti) < W-timestamp(Q),说明Q已经被更晚的事务写过了。此时不写Q,但事务Ti也不回滚,直接跳过这个写操作继续执行。

为什么可以跳过?因为W-timestamp(Q) > TS(Ti)意味着在串行顺序里,Q最终的值由更晚的事务决定。Ti写不写这个值,反正都会被更晚的事务覆盖,最终可见的Q都不会包含Ti写入的值。既然最终结果一样,那就没必要让Ti回滚,直接忽略它的这次写入即可。

6.2 这个补丁能带来多大的收益

Thomas写规则的实际收益在"写冲突频繁但读相对少"的场景非常明显。比如两个事务同时给同一个账户加钱,T1时间戳100,T2时间戳200。如果没有Thomas写规则,T2的写会触发T1的回滚;有了Thomas写规则,T1可以直接忽略写入,T2正常执行,T1的其他操作不受影响。

要注意的是,Thomas写规则只优化了"写-写冲突"的场景,对"读-写冲突"依然无能为力——TS(Ti) < R-timestamp(Q)时依然只能回滚。因为更晚的事务已经读过Q的旧值了,如果Ti再写入,那个更晚的事务就读到了一个"未来不存在"的值,这无论如何都无法忽略。

6.3 Thomas写规则的代价:视图可串行化问题

Thomas写规则不是免费的午餐。跳过写入会让最终的调度结果不满足冲突可串行化(Conflict Serializability),但满足视图可串行化(View Serializability)。简单说,有些被忽略掉的写入操作,其"效果"被后续写入覆盖了,最终结果和某个串行执行计划等价,但中间过程可能有细微的差异。绝大多数业务场景下,视图可串行化提供的一致性已经足够,但如果你在对一致性要求极高的场景下工作,需要知道这一点。

我在实现里遇到的实际情况是:Thomas写规则带来的性能提升通常非常可观,以至于它的理论瑕疵在工程上完全可以接受。但如果你要拿这个去做严格的正确性论证,建议把"启用Thomas写规则"作为系统的一个可配置项,并且保证测试覆盖到位。

7. 从单版本到多版本:MVTO的演进之路

7.1 为什么必须多版本化

基础TO协议最大的毛病,就是读规则会让读操作频繁回滚。设想一个数据项Q正在被频繁更新,W-timestamp(Q)不断变大,一个新启动的事务T1(时间戳较小)去读Q,直接触发回滚。这在读多写少的业务里是无法接受的。

多版本化的思想非常直接:保留数据的多个历史版本。每个版本都记录自己的写入时间戳W-TS和版本起止时间区间。事务读数据时,不需要检查W-timestamp是否小于自己的时间戳,而是直接查"哪个版本的时间戳区间包含我的事务时间戳",找到那个版本读就行了。这样,读操作几乎永远不会因为"读到未来数据"而回滚——它总是能找到属于自己时间点的那个版本。

7.2 版本链的实现与垃圾回收

MVTO实现的核心是版本链(Version Chain)。每个数据项的逻辑上有一个版本链表,链表从头到尾按版本新旧排列,每个节点包含:

  • 值本身
  • 版本写入时间戳(W-TS)
  • 读时间戳(R-TS),用于判断是否有更晚事务读过这个版本
  • 指向下一个版本的指针

新写入的版本插入链表头部。读操作从头部开始沿着链查找,找到第一个W-TS小于等于自己事务时间戳的版本即可。这个查找过程是O(n)的,n是版本链长度。版本链过长会导致性能下降,所以需要垃圾回收机制——定期清理那些"事务时间戳不可能再查到"的旧版本。

一个实用的GC规则是:记录系统当前最老的活动事务时间戳M,任何版本如果W-TS小于M,并且它对应的读时间戳R-TS也小于M,那这个版本永远不会再被任何事务读取,可以安全回收。

7.3 MVTO与传统MVCC的关系

很多人问MVTO和MVCC有什么区别。其实MVCC是一个更大的概念,泛指"多版本并发控制";MVTO是MVCC下的一种具体调度协议,核心特征是使用时间戳排序来决定版本可见性和冲突处理。

PostgreSQL的MVCC并不完全等同于MVTO——它的可见性判断基于事务快照(snapshot),事务看到的版本集合由快照决定,更像是一种多版本快照隔离(Snapshot Isolation)。而MVTO是明确用事务时间戳来判定每个操作的合法性,更贴近TO协议的理论框架。两者的工程实现有大量共通之处,但理论模型和冲突处理策略有区别。

8. 现实世界里谁在用TO:工业实现案例与优化思路

8.1 H-Store和S-Store的批次式时间戳排序

H-Store是学术界非常有名的内存数据库原型系统,它的设计目标就是用主内存存储加无锁并发控制来达到极致的OLTP性能。H-Store默认不采用TO,但它的研究衍生系统S-Store大量借鉴了TO的思想。S-Store把事务进行分期(epoch)批处理,同一批次内的事务使用时间戳排序来管理状态,最新的状态只属于当前批次。这种方式把TO的判定从"每条数据一个时间戳"简化为"整个批次一个时间戳",极大减少了时间戳维护的开销。

8.2 TiDB的TSO分配器是怎么设计的

TiDB作为分布式数据库,它的全局时间戳分配器PD(Placement Driver)提供了一个全局单调递增的TSO(Timestamp Oracle)。所有事务都从PD获取TSO作为自己的起始时间戳,保证全局有序。这个TSO的生成方式就是用物理时间高位+逻辑时间低位组合,配合批量分配——每个请求一次性取一批时间戳,减少跨节点通信频率。TiDB的处理方式是:内部时间戳用逻辑递增,但对外保证事务时间戳和物理时间大致对应,方便运维排查。

从TiDB的实现里可以看到,分布式数据库对时间戳的要求是"全局唯一 + 单调递增 + 尽量贴近物理时间",这三点同时满足是非常有挑战的。TSO方案本质上是引入一个中心化分配器作为时间戳来源,简单可靠但有一个中心节点热点的问题;HLC方案则不需要中心节点,但时间戳的物理时间属性有稍微宽松的取舍。

8.3 CockroachDB的混合逻辑时钟

CockroachDB使用的是HLC方案。每个节点维护自己的HLC,事务在本地启动时取HLC值作为时间戳,跨节点通信时把HLC传播给对方。由于HLC在有通信发生时才推进,两个没有通信的事务即使时间戳有偏差,也不影响正确性——因为它们之间没有依赖关系。

但CockroachDB也遇到了时间戳回退的问题:如果一个事务的timestamp小于某些已提交数据的timestamp,它就需要重试,甚至会把整个事务的timestamp推到一个更大的值重新执行。这就是"事务重启"机制,本质上是TO协议的工程变体——不直接回滚,而是换一个更大的时间戳重试。这种设计大幅降低了回滚的破坏性。

8.4 结合乐观并发控制的优化方向

最后聊一下TO和OCC(乐观并发控制,Optimistic Concurrency Control)的结合。OCC的基本思想是事务在本地私有空间执行修改,提交时统一验证——如果验证失败就回滚。TO的判定逻辑天然适合作为OCC的提交验证器:在提交阶段,检查事务的所有读写集合是否满足时间戳规则,不满足则冲突。

把TO作为提交验证器的好处是,事务执行期间完全无锁,只在提交点做一次全局判定,冲突率低时性能极好。这也是不少NewSQL系统采用的思路,比如一些基于内存事务引擎的实现,就会在提交阶段用版本号或时间戳做CAS验证。

写在后面的一点体会

从理论到工程实现,TO协议看起来只有薄薄几页纸的规则,但真正落地时你会遇到远比教科书复杂的问题——时间戳分配器的热点、旧版本回收的时机、回滚的事务要在多长时间内快速重试、日志和持久化如何配合无锁执行……每一条都需要在性能和正确性之间反复权衡。

我个人最大的体会是,做并发控制方案选型,一定要清楚自己业务里事务的特征:长短、读写比例、冲突热点、延迟敏感度。没有哪种协议是普适银弹,2PL在低冲突场景下依然高效,TO在短事务高冲突场景下确实能给到惊喜,MVCC则覆盖了绝大部分读多写少的互联网业务。理解TO的判定规则和它的变种优化,不是为了在项目里强行套用,而是为了在遇到问题时,心里多一张可打的牌。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 5:46:15

CTF压缩包爆破全攻略:从原理到实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 5:45:09

3 分钟拉齐 TikTok 账号全部作品链接:TikTokDownloader 批量获取上手

3 分钟拉齐 TikTok 账号全部作品链接&#xff1a;TikTokDownloader 批量获取上手 【免费下载链接】TikTokDownloader 抖音 / TikTok 平台作品下载/数据采集工具 项目地址: https://gitcode.com/GitHub_Trending/ti/TikTokDownloader 给它一个主页链接&#xff0c;还你一…

作者头像 李华
网站建设 2026/9/11 5:44:43

macOS OpenCV 源码编译实操:从配置到跑通全流程

macOS OpenCV 源码编译实操&#xff1a;从配置到跑通全流程 【免费下载链接】opencv Open Source Computer Vision Library 项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv macOS 上用 pip 装好的 OpenCV 包&#xff0c;控不了模块和编译参数&#xff…

作者头像 李华
网站建设 2026/9/11 5:44:19

跨平台CUDA兼容:ZLUDA让未经修改的CUDA程序跑在非NVIDIA显卡上

跨平台CUDA兼容&#xff1a;ZLUDA让未经修改的CUDA程序跑在非NVIDIA显卡上 【免费下载链接】ZLUDA CUDA on non-NVIDIA GPUs 项目地址: https://gitcode.com/GitHub_Trending/zl/ZLUDA 想在AMD显卡上跑CUDA程序&#xff0c;难道只能推倒重写&#xff1f;ZLUDA 是一个 CU…

作者头像 李华