【分布式事务连载 02】CAP、PACELC 与 BASE:老板要实时一致,架构师为何说不
系列第 2 篇|承接:单边账说明「本地 ACID 不够」|本篇回答:那我们到底能承诺多强的一致?
目标:用分区演练 + 订单状态机,吃透 CAP / PACELC / BASE,并把「最终一致」落到可执行闭环。
1. 问题引入:产品经理小美的一句话
事故复盘会上,小美拍桌子:
「能不能保证:用户点完支付,立刻、永远、两边绝对一致?做不到就别上微服务。」
老张喝了口凉水:
「能。把所有服务塞回一个库,关掉跨机房,别拆。
你要跨服务、跨机房、高可用,还要『立刻永远绝对』——物理定律先不同意。」
本篇不站队吵架,只把定律讲清楚,再讲工程怎么「认怂认得漂亮」。
2. CAP:三个字母,真正难的是分区发生时
2.1 定义用人话再说一遍
CAP由 Eric Brewer 在 2000 年提出,指的是分布式系统的三个核心特性:
| 字母 | 含义 | 用户体感 |
|---|---|---|
| C Consistency(一致性) | 所有节点同一时刻读到同一份最新数据(强一致 / 线性一致视角) | 「我在哪读都一样」 |
| A Availability(可用性) | 非故障节点必须在有限时间给出成功/失败响应,不能无限卡住 | 「系统还活着,能点」 |
| P Partition tolerance(分区容错性) | 节点之间网络断了,系统仍按设计继续运行 | 「机房光纤被挖断也有预案」 |
核心结论:在网络分区(P)必然发生的前提下,只能在 C 和 A 之间二选一。
- CP 系统:牺牲可用性,保证一致性。分区时拒绝服务,等恢复后再响应。例如:ZooKeeper、HBase、etcd。
- AP 系统:牺牲一致性,保证可用性。分区时各节点继续响应,但可能返回旧数据。例如:Cassandra、DynamoDB、Eureka。
注意:CAP 里的 C 是线性一致性(强一致),不是数据库 ACID 里的 C。ACID 的 C 是“别把数据搞坏、别违反业务规则”,CAP 的 C 是“所有节点看到的数据要一样”。
2.2 分区演练:极简购两个机房
假设订单服务主写在机房 1,只读副本在机房 2;光纤被挖断:
- CP 阵营例子:不少强一致元数据、传统 XA/2PC 协调思路——宁可失败,不给错数据。
- AP 阵营例子:DNS、很多缓存、最终一致订单查询——先让你查,稍后对齐。
2.3 和分布式事务的关系
| 事务思路 | 更偏 | 代价 |
|---|---|---|
| 2PC / XA | CP | 阻塞、延迟、吞吐差 |
| 消息最终一致 / TCC 补偿 | AP + 事后收敛 | 中间态、要幂等对账 |
| Seata AT | 更偏最终一致 | 一阶段已提交,存在可见中间态 |
宣称「我们系统同时满足 CAP」——面试口径直接判错;生产里通常是CP 或 AP 为主,再局部补强。
3. PACELC:没分区时,你也在做选择
CAP 只描述「分区时」。生产还有半句更扎心的PACELC:
PACELC由 Daniel Abadi 在 2012 年提出,是对 CAP 的补充和细化。它指出:CAP 只讨论了分区发生时的取舍,但没有分区(正常运行时)也有取舍——那就是延迟(Latency)和一致性(Consistency)之间的权衡。
若有Partition:在A与C间选;
Else(没分区):在Latency 与Consistency 间选。
P 发生 → 选 A 或 C P 不发生 → 选 L(低延迟)或 C(强一致)极简购例子:
| 场景 | 更偏 | 为什么 |
|---|---|---|
| 收银台同步扣库存再返回 | C,延迟高 | 用户要「买到」的确定感 |
| 支付成功后异步加积分 | L,短暂不一致 | 积分晚 3 秒可接受 |
| 跨城同步 2PC 转账 | C,延迟更高 | 金融不变量 |
| 日志/埋点收集 | L | 丢一点也能活 |
小美要的「立刻」,有时是L 与 C 的冲突,甚至还没到分区。把所有写都做成同步强一致,双十一 QPS 会先把你打穿。
4. BASE:不是放弃一致,是把一致推迟到可接受窗口
4.1 三个词怎么落到订单
| BASE | 含义 | 极简购落地 |
|---|---|---|
| BABasically Available | 故障时降级仍提供核心能力 | 积分服务挂了,下单支付仍可完成 |
| SSoft state | 允许中间态 | 订单:WAIT_PAY→PAYING→PAID/CLOSED |
| EEventually consistent | 无新更新后最终收敛 | 支付回调 + 超时关单 + 日终对账 |
重要:BASE不否定ACID。单库内照样 ACID;跨服务用 BASE 谈「全局怎么收敛」。
4.2 订单状态机
PAYING 是软状态窗口:库存预扣中、余额可能冻结,必须有超时与补偿。
对应动作(没有这些就不是 BASE,是「烂尾」):
| 状态/事件 | 必须做什么 |
|---|---|
进入PAYING | 库存 Try 预扣 / 超时时钟启动 |
PAID | Confirm 扣减;发「支付成功」可靠事件 |
CLOSED | Cancel 释放预扣;若已扣款则退款补偿 |
| 日终 | 订单 vs 支付单 vs 库存流水对账 |
4.3 最终一定一致怎么证明?
窗口多长算合格?业务定义,不是技术口号。
| 业务 | 可接受窗口 | 兜底 |
|---|---|---|
| 支付结果展示 | 秒级 | 主动查单 + 回调 |
| 积分到账 | 秒~分钟 | 重试 + 对账补发 |
| 跨行清算 | 小时~日终 | 清算文件对账 |
| 发票开具 | 小时级 | 人工 + 批次任务 |
5. 实战案例:支付中状态 + 超时关单
5.1 表字段
CREATETABLEtrade_order(idBIGINTPRIMARYKEY,statusVARCHAR(16)NOTNULL,-- WAIT_PAY / PAYING / PAID / CLOSEDpay_deadlineDATETIMENOTNULL,-- 超时关单时间versionINTNOTNULL,-- 乐观锁...);5.2 发起支付:进入软状态
@TransactionalpublicvoidstartPay(LongorderId){TradeOrderorder=orderMapper.selectForUpdate(orderId);if(order.getStatus()!=WAIT_PAY){thrownewBizException("状态不允许支付");}orderMapper.updateStatus(orderId,WAIT_PAY,PAYING);// 同时:库存 Try、启动超时任务(或依赖 pay_deadline 扫描)stockClient.tryFreeze(order.getSkuId(),order.getCount(),orderId);}5.3 超时关单任务防止预扣永久占用
@Scheduled(fixedDelay=5000)publicvoidcloseExpiredPayingOrders(){List<TradeOrder>list=orderMapper.selectExpired(PAYING,LocalDateTime.now());for(TradeOrdero:list){// 幂等关单intn=orderMapper.casUpdateStatus(o.getId(),PAYING,CLOSED,o.getVersion());if(n==1){stockClient.cancelFreeze(o.getId());// Cancel 必须幂等// 若支付渠道已成功,走对账纠偏,而不是盲关}}}5.4 对账兜底
每日 02:00: 支付中心成功流水 LEFT JOIN 订单 - 有支付无 PAID 订单 → 补成 PAID + Confirm 库存 - 有 PAID 无支付 → 告警人工(或自动退款流程) - 库存冻结超时未释放 → 强制 Cancel + 告警没有这步,你的「最终一致」只是希望;有了这步,才是工程。
6. 把三个理论嵌回极简购选型语言
下篇起我们进入具体协议与方案;你心里要有这把尺子:先问不变量与窗口,再问用什么中间件。
7. 踩坑总结
- 别用「最终一致」偷换 CAP 的 C。最终一致是 BASE 语境,不是「我们也有 C」。
- 中间态不设超时 = 资源泄漏(库存预扣成永久冻结)。
- 只异步、不幂等、不对账 = 永久不一致。
- 无分区时也有 L vs C:同步链路堆太多,双十一先死在延迟上。
- 对用户要诚实:积分「预计 5 分钟内到账」比「已到账」却查不到,体验更好,客服更少。
自测题
- 分区时保 C 弃 A,用户会看到什么?保 A 弃 C 呢?
- PACELC 比 CAP 多解释了什么生产现象?
- 画出你司一个订单状态机,标出每个中间态的超时与补偿动作。
下期预告
《03|2PC / 3PC:最像正经事务的方案,为何在微服务里经常变成堵车?》
我们会用「工行→建行」转账动画式时序图,把 Prepare/Commit、协调者宕机、3PC 超时误判一个个拆开。