ACID 与 BASE:两种一致性契约
先看三个真实场面:
- 转账扣了钱但没到账,用户投诉——排查发现是「扣款成功、入账失败」,中间那一步没人负责回滚;
- 「秒杀超卖」在单库上从不发生,一拆成三个库就冒出来了;
- 同一个商品页面,用户刷新两次看到两个价格,过几秒又统一了——谁都没错,只是「读到了一个还没同步完的中间态」。
这三件事的共同点:它们都不是「代码写错了」,而是「一致性的契约没签清楚」。同一份数据在几个地方各存了一份,那么「写完之后别人什么时候能看到」这件事,必须有人负责——而 ACID 和 BASE,就是两种完全不同签法的契约。
本文不从「ACID 是哪五个单词」讲起——那部分背一下就行。这里讲的是这两种契约各自承诺了什么、为此付出了什么代价,以及在真实系统里它们各自被用在哪个位置。
一、它从哪来
ACID 这个词是 1983 年造出来的。 1983 年,德国学者 Theo Härder 和 Andreas Reuter 在论文《Principles of Transaction-Oriented Database Recovery》里,把当时已经积累起来的一堆「事务应该满足什么」的共识,归纳成了一个缩写。但真正把事务做成工业标准的,是 IBM 的 Jim Gray——他在 1970 到 1980 年代关于原子性、隔离级别、恢复系统的一系列工作,构成了现代数据库事务的理论地基(1998 年图灵奖)。
四个字母各指一件事:
- A(Atomicity,原子性):一组操作要么全做,要么全不做;
- C(Consistency,一致性):事务前后,数据库的约束(主键、外键、CHECK)都成立;
- I(Isolation,隔离性):并发事务之间互相看不见对方未提交的中间态;
- D(Durability,持久性):提交之后,即使断电,数据也不能丢。
BASE 这个词则是 2008 年造的,晚 25 年。 2008 年,eBay 的架构师 Dan Pritchett 在《ACM Queue》上发表《BASE: An Acid Alternative》,提出在互联网规模下,用另一种契约去换可用性:
- BA(Basically Available,基本可用):允许部分功能降级、响应变慢,但不整体不可用;
- S(Soft state,软状态):允许系统存在中间态,中间态对外可见;
- E(Eventually consistent,最终一致):不再要求立刻一致,只要求「停止写入后,最终收敛到一致」。
这个时间差本身就是一段历史:ACID 诞生在单机数据库时代,BASE 诞生在分布式系统时代。前者要解决的是「一台机器上,多个事务怎么不互相踩」;后者要解决的是「多台机器上,一次写怎么在有网络分区、机器会挂的情况下活下来」。问题变了,答案自然跟着变。
二、为什么需要它
因为**「一致性」不是免费的,它和「可用性」和「延迟」是同一笔预算里的三个科目**。
先说没有契约的后果:
- 没有 A:扣款和入账之间程序崩了,钱就悬在中间,没人知道该不该补。这是最原始也最常见的问题。
- 没有 I:两个事务同时读同一个余额、各自扣一次,最后少了钱——这就是经典的丢失更新。
- 没有 D:提交返回成功,用户以为落定了,重启之后数据没了。这是最伤信任的一种失败,因为「成功」这个信号撒了谎。
- 没有 BASE:想跨三个机房做一次严格事务,那就必须让所有节点在写的时候达成一致——任何一个节点慢或不可达,整次写都得等或失败。这就是 CAP 里「牺牲可用性换一致性」的那条路。
所以这两种契约服务的其实是两个不同的目标函数:
- ACID 优化的是「正确性」:宁可拒绝这次请求,也不能让数据进入一个不合法或不可恢复的状态。它适合钱、库存、订单这类错了要命的数据。
- BASE 优化的是「可用性」:宁可暂时看到旧值,也要让请求能被受理。它适合点赞数、浏览计数、个性化推荐、CDN 内容这类晚几秒正确没关系的数据。
本质一句话:ACID 和 BASE 不是「好方案」和「差方案」,而是两份取舍不同的合同——ACID 承诺「任何时刻读到的都是唯一正确的值,代价是可能拒绝服务」;BASE 承诺「服务永远接受请求,代价是读到的答案在一段时间内没有唯一值」。
三、两张图看懂
先看 ACID 的四条各自在防什么。它们不是四个并列的口号,而是四道针对不同故障的闸门:
flowchart TD
T["一次事务:A 转账给 B"] --> A["A 原子性<br/>防:执行到一半崩溃"]
T --> C["C 一致性<br/>防:约束被破坏(余额为负)"]
T --> I["I 隔离性<br/>防:并发事务互相读到中间态"]
T --> D["D 持久性<br/>防:提交后断电丢数据"]
A --> R1["实现:回滚日志 / undo"]
C --> R2["实现:约束 + 触发器 + 外键"]
I --> R3["实现:锁 / MVCC 快照"]
D --> R4["实现:WAL + fsync"]
再看这两种契约在「一致性强度」这条轴上的位置。它们不是二选一,而是一条连续谱上的两端:
flowchart LR
S["强一致"] --> M["线性一致<br/>(读总能读到最新写)"]
M --> N["顺序一致"]
N --> O["可串行化事务<br/>★ ACID 的落点"]
O --> P["快照隔离"]
P --> Q["读己之写"]
Q --> R["最终一致<br/>★ BASE 的落点"]
R --> W["弱一致<br/>(不保证收敛)"]
style O fill:#ffe6e6,stroke:#cc0000
style R fill:#e6f0ff,stroke:#0055cc
两张图合起来:ACID 把赌注押在「一致性」上,BASE 把赌注押在「可用性」上。位置选在哪,取决于这份数据「错了的代价」有多大。
四、它有什么用
1. ACID 的四条,用 sqlite3 逐条实跑(本机实跑)
先说明环境:sqlite 3.53.1,WAL 日志模式。它不是分布式系统,但它是把 ACID 四条都实现得很干净的最小样本。
1 | [0] 环境 |
A —— 原子性:中间态必须能被完整抹掉
1 | [A] Atomicity(原子性):要么全做,要么全不做 |
第二段才是重点:一次违背 CHECK 的更新让整个事务回滚,总额停在 2000。原子性的价值不在于「能回滚」,而在于「回滚的粒度是整个事务,而不是一条语句」——这才是转账能安全的原因。
C —— 一致性:约束由数据库把守,不靠应用自觉
1 | [C] Consistency(一致性):约束在任何时刻都成立 |
注意 affected = 0:更新一个不存在的账户不会报错,只会影响 0 行。这是很多人踩过的坑——应用如果不检查影响行数,就会把「什么都没做」当成「做成功了」。一致性这一条里,最容易漏的从来不是数据库,是应用层没看返回值。
I —— 隔离性:读不到「别人改了但没提交」的值
1 | [I] Isolation(隔离性):并发事务互相看不见未提交的中间态 |
这里有一个细节值得展开:读事务没有阻塞,它读到了 1000(旧值),而不是 100(未提交的新值)。这体现的是「快照隔离」——读者拿到的是一份一致的旧快照,而不是最新值。所以严格来说,隔离性保护的不是「读到最新的正确值」,而是「读到一个不掺假的值」。
D —— 持久性:COMMIT 之后数据活在日志里
1 | [D] Durability(持久性):COMMIT 返回后,数据必须活过崩溃 |
COMMIT 返回之后,数据其实还在 WAL 里,主库文件还是 4096 字节的原始大小。这个观察解释了为什么「持久性」和「性能」永远在打架:如果每次提交都去改主库文件并 fsync,代价是随机写 + 一次磁盘同步,慢得多;而追加 WAL 是顺序写,快得多。这就是「写前日志 WAL」那条母题在事务里的落点——把随机的、分散的写,换成顺序的、集中的写。
2. 同样一次转账,放到 BASE 的 3 个副本上(本机实跑)
现在把「转账」搬到一个最终一致的模型上,看同样四条是怎么被逐条放宽的:
1 | [BASE 对照] 把「转账」放到 3 个最终一致的副本上:ACID 的四条被逐条放宽 |
三个副本读到三个答案 [200, 1000, 1000]——这不是 bug,这是 BASE 明确写进合同的条款。然后在反熵(anti-entropy)推进下,第 8 轮收敛回 [1000, 1000, 1000]。
把四条放宽一一对上:
| ACID 的条款 | BASE 里的对应放宽 | 放掉的代价 |
|---|---|---|
| 原子性:全做或全不做 | 局部提交:先扣一边,另一边稍后补偿 | 中间态可能在系统里存在一段时间 |
| 一致性:约束时刻成立 | 软状态:允许中间态对外可见 | 读到「不合逻辑」的值是允许的 |
| 隔离性:并发互不可见 | 无全局隔离:读到的可能是旧值 | 需要业务能容忍陈旧读 |
| 持久性:提交即落定 | 最终持久:靠反熵 / 补偿达成 | 收敛时间无硬上限(只有工程上限) |
这里最需要注意的是「补偿」这个词:BASE 不是「不保证正确」,而是把保证从「数据库事务」搬到了业务逻辑里。转账在 ACID 里靠 ROLLBACK 撤销,在 BASE 里得靠对账 + 冲正去修。工作量没有消失,只是从数据库挪到了应用层——而且从「自动」变成了「你要自己写」。
3. 那实际系统里到底该选哪个
答案不是二选一,而是按数据类型分线:
1 | 数据类型 契约 典型实现 为什么 |
这张表的读法是:先问「这份数据错了会怎样」,再决定签哪份合同。而不是先选技术栈,再想办法圆回来。
五、反例与边界
- ACID 的「C」其实是应用的责任。 数据库能保证的只是一部分约束(主键、外键、CHECK)。「每个订单必须属于一个有效用户」「库存不能被两个订单同时占用」这类业务一致性,靠的是事务 + 正确的隔离级别 + 应用逻辑三者合力。把 C 全甩给数据库,是最常见的误解。
- 隔离级别不是「有/无」,是四档。 从「读未提交」到「可串行化」,中间还有「读已提交」「可重复读」。级别越高,越安全,也越容易阻塞或产生冲突重试。选错级别(比如在电商扣库存时用了「读已提交」)会引进幻读和丢失更新。
- BASE 不等于「随便不一致」。 它承诺的是最终一致,前提是「没有新的写入」。如果写入永不停歇、或者补偿逻辑有 bug,系统可能永远收敛不了——那就不是 BASE,是「不一致」。BASE 需要能度量收敛时间(lag),否则它只是一句口号。
- BASE 的代价被低估得最狠。 ACID 的成本(锁、日志、同步)是在数据库里,会被压测暴露出来;BASE 的成本(对账、补偿、幂等、去重)是在业务代码里,要等到真出问题才暴露。所以「看起来更简单的方案」,往往是把复杂度藏到了更晚的地方。
- 别把 CAP 和 ACID 混着谈。 CAP 说的是网络分区时在一致性(C,指线性一致)和可用性(A)之间取舍;ACID 的 C 是事务约束,两者根本不是同一个 C。这是分布式领域最经典的术语陷阱。
- 同城强一致 + 异地最终一致,是当下最常见的折中。 它不是「ACID 派」或「BASE 派」,而是按距离分层:同一机房内做到强一致(延迟低、能接受同步开销),跨地域用异步复制收尾。一致性强度不必全局统一,可以按故障域分段。
六、对比表与小结
| 维度 | ACID | BASE |
|---|---|---|
| 一致性强度 | 强(事务级唯一值) | 弱 → 最终收敛 |
| 可用性 | 分区时可能拒绝服务 | 优先接受请求 |
| 延迟 | 高(锁 + 同步 + fsync) | 低(本地提交,异步传播) |
| 复杂度位置 | 在数据库内部 | 在业务代码里(对账 / 补偿 / 幂等) |
| 出错时谁负责 | 数据库回滚 | 业务补偿逻辑 |
| 典型场景 | 钱、库存、订单、权限 | 计数、缓存、推荐、搜索索引 |
| 代表实现 | 单机 ACID 数据库 / 分布式事务 | 多副本 + 反熵 / CDC / 消息队列 |
| 失败模式 | 请求被拒(用户重试) | 读到旧值(业务容忍) |
| 你要写的一段逻辑 | 选 ACID 会怎样 | 选 BASE 会怎样 |
|---|---|---|
| 「扣库存 + 建订单」 | 一个事务,要么都成要么都不成 | 先扣后建,失败了对账冲正 |
| 「加一次阅读量」 | 每次都要写库,热点竞争 | 本地计数 + 定时归并,快得多 |
| 「改密码」 | 事务提交后才返回成功 | 不适用(安全边界必须强一致) |
| 「同步用户资料到搜索」 | 不现实(跨系统无法事务) | CDC / 消息队列,最终一致 |
🐾 小结:ACID 和 BASE 常被讲成「传统 vs 现代」「优 vs 劣」,这个框架从一开始就错了。它们是两份取舍不同的合同:ACID 拿可用性和延迟,换「任何时刻都只有一个正确答案」;BASE 拿「暂时没有唯一答案」,换「请求永远被受理」。真正要练的能力不是背四个字母,而是在看到一份数据时,先问一句「它错了会怎样」——答案决定了你该签哪份合同。至于「BASE 更简单」这个错觉,只要记住一句话就够了:事务的回滚是数据库替你写的,BASE 的补偿得你自己写。
相关阅读:

