资讯动态

CAP与BASE:分布式事务的底层逻辑与柔性方案解析

发布时间:2026/10/19 9:58:41 来源:尧图企业网站定制
聊分布式事务绕不开CAP和BASE这两个地基概念。我第一次正经面对它们是在一个电商项目的订单拆分评审上系统从单库拆成订单、库存、积分三个服务之后原来一个数据库事务能搞定的事瞬间变成跨三个系统的调用链。当时负责的同事脱口而出“我们得引入某个分布式事务中间件吧”但问到“你准备在分区故障下牺牲哪头”的时候会议室安静了十几秒。CAP和BASE就是用来回答这类问题的。这篇文章我会按一套比较接地气的顺序来写先用一个真实的业务困境说明分布式事务到底难在哪然后再拆解CAP定理的每一个字母、为什么三者不能兼得接着进入BASE理论和ACID的对比最后把常见的柔性事务方案本地消息表、TCC、SAGA等放进CAP坐标系里对照。如果你正准备给系统引入分布式事务方案或者正处在“要不要用强一致”的纠结里这篇能帮你把底层逻辑捋顺。1. 先从一次“钱货两清”的失败说起1.1 订单从单库拆到多服务之后我第一次意识到事务会失效模拟一下当时的场景。我们有一个订单系统早期所有数据都在同一个数据库里订单表、库存表、积分表。用户下单时程序在一个数据库事务里完成三件事——生成订单记录、扣减库存、给用户加积分。因为同一套undo/redo日志在管所以任何一步失败整个事务都能回滚数据永远是一致的。这种日子过得很安心直到流量涨上来库存和积分变成了独立服务。拆完之后下单流程变成了先调订单服务写入订单再调库存服务扣库存再调积分服务加积分。三个服务各自用自己的数据库本地事务只能管到自己的那一亩三分地。某个周末的晚上库存服务正常扣减成功积分服务却因为网络抖动超时了订单服务收到异常后把订单回滚成“未支付”但库存已经扣掉了。用户在页面上看到的是“支付失败”后台库存却莫名其妙少了一截。第二天对账脚本跑出来一堆差异大家才意识到单机事务的保护伞在这里已经失效。1.2 分布式事务到底在解决什么把这次故障拆开看分布式事务要解决的核心问题其实和单机事务差不多只是战场变了原子性跨多个服务/数据库的多个写操作要么全部成功要么全部不生效。但每个服务的本地事务只能提交或回滚自己的部分没有全局的“回滚开关”。一致性多个服务各自维护数据副本彼此之间的数据要在某个时刻对齐。比如库存扣了订单就得是成功状态积分没加至少也得有补偿。隔离性并发操作下中间状态不能被其他请求看到。单库里可以用锁和MVCC做到跨服务之后这个能力非常勉强。持久性这个相对好办每个服务自己保证落盘即可。问题的根源在于单机事务的基石是共享内存里的锁、本地日志、单点协调器。跨进程之后这些资源都不再共享你没法用“同一个事务管理器”去控制所有操作的命运。所以业界才从理论层面提出了另一套标准——先看CAP再看BASE。它们不是某种具体中间件而是帮你在设计阶段决定“分区发生的时候我打算怎么表现”。2. CAP定理分布式系统的不可能三角2.1 C、A、P分别代表什么CAP定理是计算机科学家在研究分布式系统时总结出的一个约束关系。它说一个分布式系统在一致性Consistency、可用性Availability、分区容错性Partition Tolerance这三个属性里最多只能同时满足两个。先给出精确定义C一致性所有节点在同一时刻看到同一份数据。严格意义上是“线性一致性”——如果A节点写入了一个新值那么之后任何节点的读操作都必须读到这个新值哪怕请求打到别的节点上。A可用性系统收到请求后必须在有限时间内给响应。注意这里说的“给响应”不等于给正确响应只要不报错、不无限等待就算可用。P分区容错性当节点之间的网络通信发生中断比如交换机故障、丢包、机房隔离系统依然能继续对外提供服务。用一个生活化类比一群人在电话会议里对口径。C是所有人说话口径完全一致A是电话必须能接通、能说话P是哪怕有几个人掉线了会议不能直接散场。你很快会发现三者之间存在天然的冲突。2.2 为什么三者不能同时满足一个简单的论证CAP为什么是“不可能三角”不需要堆数学公式一个两节点思想实验就够了。假设有两个节点N1和N2它们之间发生了网络分区互不通信。现在客户端向N1发起写请求把x的值从0改成1。分区期间另一个客户端向N2发起读请求问x等于多少。如果系统坚持P分区下继续服务N2必须响应这个读请求。可N2没有收到N1的同步手里x还是0。这时候只有两条路返回0旧值系统可用性满足了但一致性被破坏——读到的不是最新值。不返回、等N1同步一致性满足了但因为网络分区N1的消息永远传不过来N2只能无限期等待这等于放弃可用性。无论怎么选C和A在P存在时必然牺牲一个。结论就是分布式系统在分区发生时只能在“继续服务但可能给出旧数据”和“保证数据一致但可能拒绝服务”之间二选一。2.3 网络分区是常态所以真正选项只有两个很多人把CAP理解成“分布式系统必须在C、A、P里三选二”这个说法不够准确。因为它暗示P是可选项——可实际上只要是分布式系统节点之间的网络就不可能永远畅通。宕机、GC停顿、网络抖动、交换机维护……分区是常态而非例外。所以在实际工程里P不是“选不选”的问题而是“默认必须选”的问题。真正需要决策的是在分区发生的那段时间里系统倾向CP还是倾向AP这里有个容易被误解的地方——不是说要始终放弃其中一个。没有分区的时候所有节点之间可以正常通信C和A都能满足系统表现得既一致又可用。只有当分区真实发生时矛盾才被触发这时你希望它进入“宁可暂时不可用也不要错误数据”模式还是“宁可返回旧数据也不要中断服务”模式这才是CAP在选择层面的真正含义。用实际场景举例注册中心、配置中心这类元数据系统通常会选CP。配置写错了会影响全局宁可短暂不可用也不能让两个节点持有不同的配置。而商品详情、用户浏览、推荐列表这类读多写少的系统通常选AP。返回一条稍微过期的商品库存对用户体验的伤害远小于“页面打不开”的伤害。3. BASE理论向现实妥协的最终一致模型3.1 BASE是怎么来的CAP定理给出了一个二元选择分区时要么一致、要么可用。但早期做大型互联网电商的那批架构师发现现实业务很少真的需要在两者之间做“非黑即白”的选择。很多操作其实可以接受一个中间的、过渡的状态——只要它最终能收敛到正确的结果。于是他们在实践中总结出一套更贴近真实业务的原则英文缩写恰好是BASE中文叫“碱”和ACID酸形成了一个有意思的对仗。BASE由三句话组成基本可用Basically Available、软状态Soft State、最终一致Eventually Consistent。它的核心思想是不要把“原子性和一致性”当成跨系统协作的唯一出路而是允许系统在一段时间内处于中间状态通过异步补偿、重试、对账等手段让数据最终对齐。3.2 基本可用、软状态、最终一致分别是什么基本可用Basically Available是指系统在出现故障时允许损失部分可用性而不是整个系统瘫痪。注意这里的关键词是“部分”。比如大促流量高峰时把非核心的“猜你喜欢”接口降级把用户的评论列表从实时查询改成缓存数据甚至把下单前的某些校验改成异步执行。用户看到的功能少了一点、响应慢了一些但核心链路还在走。这叫基本可用。软状态Soft State是指允许系统内的数据副本存在中间状态而且这个中间状态会随时间变化。举个例子库存服务扣减了库存但积分服务还没加上积分此时两个服务之间的数据就处于软状态。过一会儿积分服务收到了消息积分才加上。软状态本身是允许存在的不需要像ACID那样一直保持“要么全有要么全无”。最终一致Eventually Consistent是BASE的核心承诺在没有新的写入且经过足够时间后所有副本的数据会收敛到同一个值。它强调的是“最终”而非“立刻”。比如刚才的库存和积分最后一定会对齐如果积分服务一直没收到消息系统里还会有补偿任务把差额补上。这里面有一个工程上极其重要的潜台词最终一致不是无限重试、不是放任不管而是必须定义收敛的时间窗口和兜底手段。3.3 从ACID到BASE变化的不只是“一致”两个字ACID和BASE经常被放在一起对比但它们不是同一层级的替代关系。ACID针对的是单机数据库事务描述在一个数据库实例内部如何保证可靠BASE针对的是跨服务、跨数据库的分布式协作描述在一个更大范围内如何允许局部不一致、最终对齐。维度ACIDBASE原子性要么全成功要么全回滚拆成多个子操作各自提交通过补偿到达目标一致性事务前后数据约束不变允许短暂不一致最终收敛隔离性并发事务互不干扰中间状态可能被看到需要业务层面容忍持久性事务提交后数据不丢失数据不丢失但可能延迟可见我在实际项目里见过不少刚接触分布式的人容易把BASE理解成“可以在数据库里乱写数据后面再说”。这个理解很危险。BASE从来没有放弃持久性和最终正确性它只是把一致性从“事务提交的那一刻”推后到了“某个可接受的时间点”。换句话说如果在最终收敛时间内没有补偿成功系统应该告警、停机、人工介入而不是任凭脏数据躺在那。3.4 最终一致性也有强弱之分“最终一致”听起来像一个笼统的词实际上它包含若干强度不同的子模型。工程上最常用的是两种读己之所写Read-your-writes用户提交了订单之后立刻去查一定能查到自己的订单但其他用户可能还没看到这条订单。这在大促场景里几乎是标配——不然用户刚下完单刷新页面订单不见了会直接投诉。单调读Monotonic read用户在时间轴上的多次读取不会出现“第一次读到新值第二次读到旧值”的倒退否则用户会认为系统状态错乱。理解这些子模型很重要因为它们在选型时会直接影响方案设计。比如“读己之所写”要做成的话往往需要把用户维度的路由固定到某个节点上或者引入会话亲和性如果你选的方案做不到这一点就得在业务层加补偿逻辑。后面聊具体方案时会反复碰到这些概念。4. 把理论套到工程上柔性事务方案坐标系4.1 本地消息表把跨系统写变成“本地事务异步重放”理论说完了工程上怎么落地这里开始讲真正的柔性事务方案。第一个入门方案是本地消息表它也是我觉得最容易理解和实现的一个。它的做法是把“更新业务数据”和“登记一条待发送消息”放在同一个本地数据库事务里。以订单系统和积分系统为例下单时在订单数据库里同时做两件事——写入订单记录、往本地消息表插入一条“给用户加积分”的消息。这两个动作共享同一个数据库事务所以要么一起成功要么一起失败。事务提交之后后台再启动一个定时任务扫描本地消息表把状态为“待发送”的消息发给积分服务的队列或接口积分服务处理完成后返回成功本地任务把消息标记为“已发送”。这个方案的核心洞察在于跨两个系统的原子性被巧妙地转换成了“本地事务的原子性异步消息重试的最终一致”。数据库事务依然在发挥作用只是它的保护范围从“多个系统”缩小到了“单库内的两张表”。至于跨系统的部分交给消息重试去对齐。当然它有代价。消息表需要清理否则会一直膨胀消息发送可能重复对端自己要做好幂等如果对端一直处理失败本地任务会反复重试需要有告警和人工介入。但从CAP坐标来看它牺牲了极端场景下的一致性你允许积分延迟到账换取系统的整体可用性属于典型的AP倾向方案。4.2 事务消息把本地消息表搬进消息队列本地消息表虽然思路清晰但消息表维护成本不低。于是消息队列厂商把它内置成了“事务消息”功能。以某主流MQ为例它的流程是生产者先发送一条“半消息”对消费者不可见然后执行业务本地事务如果本地事务提交成功生产者对半消息发起commit消息变得对消费者可见如果本地事务回滚则rollback消息被丢弃。和本地消息表相比事务消息把“待发送消息”的可靠存储、重试机制全部交给了消息队列业务方不需要自己建表扫表。但它有两个额外要求一是消息队列本身必须高可用否则半消息阶段也可能丢失二是消息队列会提供“回查”机制——如果半消息一直没等到commit它会反向问业务方“这个事务最后到底成功没有”业务方需要实现一个回查接口。我第一次接到这种需求时以为就是发发消息的事结果发现回查接口才是真正的重头戏它要求业务方能在不确定的状态下给出确定答案。这里的核心原则是业务操作记录必须能被查到最终状态不然回查没法回答。4.3 TCCTry、Confirm、Cancel三段式补偿再往下走事情就开始变复杂了。TCC是目前非常常见的柔性事务方案它的名字来自三个阶段Try预留资源、Confirm确认执行、Cancel释放资源。以资金操作为例。假设一个操作要同时扣减用户A的余额、增加用户B的余额这两个操作分布在两个服务里。TCC的设计就是每个服务都要提供三个接口Try阶段做资源预留——A扣减的款项先冻结B增加的额度先预占注意这时并没有真正生效Confirm阶段把预留的资源正式生效——A的冻结变扣减、B的预占变增加Cancel阶段把预留的资源释放——如果某个服务在Try之后发现无法继续其他服务执行Cancel把冻结核销。对比前面几种方案TCC的优点是每个阶段的动作都是本地事务协调者只负责状态编排不需要像2PC那样持有全局锁资源占用时间短、冲突概率低。对应的代价是业务方要额外设计并维护三套接口代码复杂度直线上升而且每个阶段都可能因为网络问题失败所以Confirm和Cancel本身必须支持重试和幂等。如果你们的团队缺乏足够的中间件支撑只为了一个中间操作上TCC很可能会被接口数量拖垮。4.4 SAGA长事务拆成小步骤失败就逆向补偿另一个常见的柔性事务方案是SAGA。它的核心思想是把一个长事务拆成一组有顺序的本地事务每个本地事务对应一个正向操作和一个补偿操作。正常流程走完所有正向操作如果第n步失败就去执行第n-1步、第n-2步……的补偿操作一步步退回起点。SAGA特别适合那种流程很长、步骤很多、而且每步天然独立的业务比如旅行预订订机票、订酒店、订租车三个环节属于不同的服务你可以分别提交订单一旦某个环节失败就取消前面已经成功的订单。它的实现比TCC直观因为不需要预留资源每步都是真实提交。缺点是中间状态对外可见在SAGA执行过程中别的事务可能会看到“机票已经订了但酒店还没订”的中间数据如果业务上不能容忍这种部分完成的视角SAGA就不合适。4.5 放在一起看各种方案的CAP落点和选择逻辑方案核心机制CAP倾向适合场景主要风险本地消息表本地事务消息表定时扫描AP最终一致内部系统间异步通知、对一致性要求不高的辅助数据消息表膨胀、重复投递事务消息半消息本地事务回查AP最终一致跨系统可靠投递、希望减少表维护成本依赖MQ高可用、回查接口复杂度TCCTry-Confirm-Cancel三阶段AP最终一致资金、库存等需要短平快释放资源的场景三套接口维护成本高、需要强幂等SAGA本地事务序列补偿AP最终一致长流程业务、每步天然独立中间态可见、补偿链条复杂2PC全局协调者两阶段提交CP强一致规模小、强一致要求极严的内部调用协调者单点、阻塞风险、吞吐有限表格里特别提到2PC是因为很多人在想“分布式事务”时第一反应还是它。2PC确实能提供强一致但它对协调者依赖极高协调者一旦出现故障参与方会长时间阻塞而且在网络分区环境下它本身更像是选择了CP——宁可让整个事务卡住也不允许出现不一致。现代高并发系统里2PC的使用场景已经很窄更多是作为理论参照存在。5. 理论真正教会我的几件事5.1 强一致不是所有业务的必需品我观察到一个很普遍的现象开发同学刚接触分布式事务时总想把所有操作都武装到牙齿好像不用上强一致就会出大事。但仔细一分析业务就会发现用户对“积分晚10分钟到账”完全没有感知对“下单时页面转圈30秒”却会立刻流失。BASE理论教的第一课是量化你能接受的不一致窗口是秒级、分钟级还是24小时内对账能发现即可这个数值是选型的尺子也是最容易被跳过的一步。5.2 说“最终一致”时要能把“最终”定义清楚“最终一致”这四个字如果管理不好就会变成“永远不一致”。真正的最终一致必须有明确的收敛目标、重试次数、超时时间和告警阈值。我在做一个模拟订单系统时给最终一致下的定义是订单状态变更后相关下游系统的同步任务必须在30秒内感知到变化超过5分钟未同步触发告警超过30分钟进入人工补偿流程。不定义这些数字最终一致就只是房间里的一句口号线上出了故障没人能说清楚“最终”到底多久到。5.3 幂等是分布式事务的地基引入重试和补偿之后重复消息就成了家常便饭。处理不好一个补偿任务会把用户的积分加两次。解决手段其实很朴素给每一次业务操作定义全局唯一的幂等键——比如“订单号操作类型”在下游服务的数据库表里加唯一约束重复请求到库时唯一索引直接挡住接口返回上一次的结果即可。别追求什么高深技巧一张带唯一索引的幂等表加一个状态字段就能解决绝大多数重复投递问题。5.4 选型前的理论检查清单现在再回头看开头的那个会议室场景我一般会在争论“用哪个中间件”之前先强制大家过一遍四个问题这个跨系统写操作能不能改造成“单本地事务异步消息”如果链路中途失败补偿流程由谁负责反向接口怎么设计最终的数据不一致是靠对账发现的还是靠用户投诉发现的对账周期多久并发场景下中间状态会不会被业务逻辑读到如果会是否在代码里做了兼容把这四个问题过完大多数情况下你会发现自己真正需要的并不是那个最复杂的方案。我在这些年的实践中见过太多因为没想清楚“分区时的表现”而把系统架构搞到失控的例子。CAP和BASE不是停留在书上的名词它们是你做每一个跨系统设计决策时手里最基础的那张地图。下一篇文章我会从本地消息表开始完整地拆一个能落地的柔性事务设计出来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑