ACID vs BASE
ACID 四个字母各由数据库里的哪个机制兑现:atomicity 靠日志回滚、consistency 靠约束、durability 靠 WAL 刷盘,以及 PostgreSQL synchronous_commit、InnoDB innodb_flush_log_at_trx_commit、Redis appendfsync、MongoDB write concern 分别让你丢多少数据。BASE 系统怎么用幂等和补偿收敛,附丢数据算例和面试答法。
这一章回答三个问题:ACID 每个字母在数据库里由哪个机制兑现、durability 的配置到底能让你丢多少数据、放弃 ACID 的 BASE 系统靠什么最终对齐。 隔离级别和并发异常的细节在 Transactions,跨服务的 Saga、2PC、outbox 在 分布式事务,这里不重复。
ACID 和 BASE 不是两种数据库,而是两种保证的说法。同一个系统里通常两者并存:订单和支付走 ACID,点赞数、推荐列表、搜索索引走 BASE。面试里说「我用 NoSQL 所以是 BASE」会被追问;说清楚「哪类数据丢了或者晚到了代价是什么」才是答案。
有约束的设计问题
一个电商平台,高峰 2,000 笔订单/秒,有三类写入:
- 订单与支付:用户看到「支付成功」之后,这笔订单任何情况下都不能消失。
- 积分:每笔订单完成后给用户加积分,晚几秒到账可以,但不能加两次,也不能漏加。
- 商品浏览计数:每秒几万次自增,服务器崩溃丢掉最近一秒的计数没人在意。
三类数据对「提交后还能不能丢」「多久之后必须一致」的要求完全不同,下文每一节都回到这三类数据。
ACID:每个字母对应一个机制
ACID 代表 Atomicity、Consistency、Isolation、Durability,用于在 transactions 处理中保证 data integrity。
| 字母 | 保证什么 | 数据库里靠什么兑现 | 应用还要做什么 |
|---|---|---|---|
| Atomicity | 事务里的写要么全部生效,要么全部撤销 | 日志(WAL / redo + undo);崩溃恢复时重做已提交的、撤销未提交的 | 把必须一起成功的写放进同一个事务 |
| Consistency | 事务结束时数据满足声明的规则 | PRIMARY KEY、FOREIGN KEY、UNIQUE、CHECK、exclusion 约束 | 数据库管不到的业务规则(如「余额不能透支」没写成约束)由应用负责 |
| Isolation | 并发事务互不看到对方的中间结果 | 锁、MVCC、SSI | 选隔离级别,按级别写重试;详见 Transactions |
| Durability | 返回提交成功之后,崩溃也不丢 | 提交时把日志刷到持久存储(fsync),再加复制 | 确认刷盘策略和复制方式与数据的价值匹配 |
原文对四个字母的定义:
Atomic
一个 transaction 的所有操作要么全部成功,要么全部 rollback。MySQL 文档写明,InnoDB 的崩溃恢复不受刷盘设置影响,事务要么完整应用,要么完整抹去。
Consistent
Transaction 完成后,database 处于满足约束的状态。注意这个 C 和 CAP 里的 C 不是一回事:ACID 的 C 说的是约束和不变量,CAP 的 C 说的是多副本读到最新值。数据库只能保证你声明给它的规则;「库存不能为负」如果没写成 CHECK (stock >= 0),就只能靠应用。
Isolated
Transactions 之间互不干扰。Database 协调并发访问,让 transactions 看起来像顺序执行。实际数据库默认都不是完全串行:PostgreSQL 默认 Read Committed,InnoDB 默认 Repeatable Read,各自允许的异常不同。
Durable
一旦 transaction 提交成功,即使系统故障,数据也会保留。关键在「提交成功」是什么时候返回的:日志刷到磁盘之后返回,才是真的 durable;只写进内存或操作系统缓存就返回,崩溃时会丢。下一节就是这个区别。
Durability 是一组配置,不是开关
几个主流系统里控制「提交返回前日志要落到哪里」的参数:
| 系统 | 参数 | 默认值 | 放松后的行为 |
|---|---|---|---|
| PostgreSQL | synchronous_commit | on:等本地 WAL 刷盘 | off:不等刷盘就返回,文档写明最坏延迟是 wal_writer_delay 的三倍;wal_writer_delay 默认 200 ms,即最多约 600 ms 的已确认事务可能丢失 |
| MySQL InnoDB | innodb_flush_log_at_trx_commit | 1:每次提交写日志并刷盘,文档称这是完整 ACID 所必需 | 0:每秒写并刷一次;2:每次提交写日志、每秒刷一次。两种设置下崩溃都可能丢最多约 1 秒的事务,文档说明每秒刷盘不是 100% 准时 |
| Redis AOF | appendfsync | everysec | always 每条命令 fsync;everysec 最多丢约 1 秒写入;no 交给操作系统决定何时刷 |
| MongoDB | write concern w 和 j | 多数部署默认 { w: "majority" } | w: 1 只等 primary 确认;j: false 不等 journal 落盘 |
两个容易说错的点:
- PostgreSQL 的
synchronous_commit = off和fsync = off不是一回事。 文档说前者只会丢最近的事务,数据库状态和这些事务被干净回滚一样,不会损坏;后者是整个服务器级别的设置,可能导致数据损坏。前者还可以按事务设置:一个事务用哪种模式,取决于它开始提交时的synchronous_commit值。 - 单机刷盘不等于不丢。 刷盘保护的是进程或机器崩溃;磁盘整块坏了、机房断电后主库起不来,要靠复制。异步复制时 primary 上已提交的事务在 failover 后可能不在新 primary 上,这部分见 Database Replication 和 PACELC。MySQL 文档也提醒,开了 binlog 的复制环境要同时设
sync_binlog=1。
算一遍:放松刷盘会丢多少单
用开头的 2,000 订单/秒,数据库崩溃一次:
- InnoDB
innodb_flush_log_at_trx_commit=2:最多约 1 秒未刷盘,约 2,000 笔已经告诉用户「支付成功」的订单可能不存在。刷盘偶尔晚于 1 秒时会更多。 - PostgreSQL
synchronous_commit=off:最多 3 × 200 ms = 600 ms,约 1,200 笔。 - 两者都用默认值:0 笔丢在本机层面;代价是每次提交都要等一次日志刷盘。数据库会把同时提交的多个事务合并成一次刷盘(group commit),所以吞吐不是被磁盘单次延迟直接除出来的,具体数字要在自己的硬件上测。
结论很直接:订单库保持默认;浏览计数这类数据,可以在事务级别放松(PostgreSQL 里给这类写入的事务单独设 SET LOCAL synchronous_commit = off),或者干脆不放进这个库。
BASE:放弃即时一致,换可用和扩展
随着数据量增长和高 availability 需求,很多系统把部分数据交给不做跨记录事务的存储,放宽对即时一致性和数据新鲜度的要求,以换取 scale 与 resilience。
BASE 这个说法最早出现在 Fox、Brewer 等人 1997 年的 SOSP 论文 Cluster-Based Scalable Network Services,原文把它定义为 Basically Available、Soft State、Eventual consistency,并说「不严格满足 ACID 的数据语义都是 BASE」。Dan Pritchett 2008 年在 ACM Queue 的《BASE: An ACID Alternative》把它用到了数据库的功能拆分和异步更新上。BASE 与 ACID 没有严格的一一对应:
Basically Available
系统在部分节点故障时仍然响应,可能返回旧数据或部分结果,而不是整体报错。
Soft state
不同 replicas 在某一时刻不要求相同,状态会在没有新写入时继续变化(复制、修复还在进行)。
Eventual consistency
没有新写入后,所有副本最终收敛到同一个值。期间系统仍可读,只是读到的可能不是最新值。
「最终」不是一个时间承诺。设计 BASE 系统必须回答两件事:正常情况下收敛要多久(要监控),收敛之前读到旧值用户会看到什么。
让 BASE 真正收敛:幂等 + 补偿
以积分为例。订单事务提交后发一条「订单完成」消息,积分服务消费后加分。消息队列通常是至少一次投递,同一条消息可能来两次;积分服务也可能在加完分、确认消息之前崩溃,然后重新消费。
Pritchett 那篇文章用的就是这个思路:用持久化消息队列异步更新汇总数据,再用一张记录「已经应用过哪些消息」的表让重复消息不会被加两次。写成 SQL:
BEGIN;
INSERT INTO applied_events (event_id) VALUES (:event_id); -- 主键冲突说明处理过,回滚并确认消息
UPDATE user_points SET points = points + :delta WHERE user_id = :user_id;
COMMIT;
-- 提交后才确认(ack)消息
去重记录和积分变更在积分服务自己的库里是同一个 ACID 事务,这一步是局部 ACID。整体链路(订单库 → 队列 → 积分库)是 BASE:两个库在几百毫秒到几秒内不一致,但不会多加也不会漏加。
订单先提交再发消息,中间崩溃会漏发,这一步用 outbox:把「要发的消息」和订单写在同一个事务里,再由单独的进程读出来发送。订单被取消时,积分不能「回滚」,只能发一条反向的补偿事件扣回去。这两个机制的完整拓扑见 分布式事务。
读的一侧也可以选。DynamoDB 的读默认是最终一致的,GetItem、Query 可以传 ConsistentRead: true 拿最新值,文档写明最终一致读的成本是强一致读的一半,而 GSI 只支持最终一致读。所以「用户刚改完资料立刻查看」走强一致读,列表页走最终一致读。
常见翻车
| 翻车 | 用户看到什么 | 修法 |
|---|---|---|
为了压测好看把 innodb_flush_log_at_trx_commit 改成 2,忘了改回来 | 数据库主机崩溃后,最近一秒的「支付成功」订单查不到 | 核心库保持 1;只在可丢数据的库上放松,并写进配置审计 |
以为 fsync = off 只是「快一点」 | 断电后数据库启动报数据页损坏 | 不在生产关 fsync;需要快用 synchronous_commit = off |
| 单机刷盘但异步复制,failover 到落后的副本 | 切换后几秒内的订单消失 | 核心数据用同步或半同步复制,或 failover 前确认副本追平 |
| 业务规则只写在应用里,没有约束 | 两个服务版本并存时写入违规数据 | 能写成 CHECK / UNIQUE / FOREIGN KEY 的规则交给数据库 |
| BASE 消费者不幂等 | 积分偶尔加两次 | 去重表或条件写,和业务变更放在同一个本地事务 |
| 「最终一致」没有监控 | 某个消费者卡住两小时,用户投诉积分不到账才发现 | 监控复制滞后、消费延迟和积压,设告警阈值 |
怎么选,回到三类数据
- 订单与支付:ACID,默认刷盘,同步或半同步复制,约束写在库里。
- 积分:跨服务用 BASE,靠 outbox + 至少一次投递 + 幂等消费收敛;积分库内部的「去重 + 加分」仍是一个 ACID 事务。
- 浏览计数:BASE,可以放在 Redis 里用
appendfsync everysec或更松的设置,丢一秒可以接受;定期汇总进数据库。
面试时这样回答
- 先按数据分类。 「订单不能丢、积分可以晚到但不能重复、计数可以丢一秒。」
- 把 durability 说成配置。 点名参数和默认值:InnoDB
innodb_flush_log_at_trx_commit=1、PostgreSQLsynchronous_commit=on,并说出放松后的丢失窗口(约 1 秒、约 600 ms)。 - 说清 C 是谁保证的。 数据库只保证声明过的约束,业务规则能下沉就下沉。
- BASE 要说怎么收敛。 outbox 保证消息不漏发,幂等消费保证不重复,补偿事件处理撤销;给出监控的滞后指标。
- 说一个故障。 例如异步复制 failover 丢了已提交的订单,修法是同步复制或切换前确认副本追平。
相关章节:Transactions、分布式事务、Database Replication、PACELC Theorem、Consistency Patterns、SQL databases、NoSQL databases。
一手证据
- PostgreSQL:Asynchronous Commit
- PostgreSQL:WAL configuration(synchronous_commit、wal_writer_delay)
- MySQL:InnoDB Startup Options and System Variables(innodb_flush_log_at_trx_commit)
- Redis:Persistence(appendfsync)
- MongoDB:Write Concern
- PostgreSQL:Constraints
- Amazon DynamoDB:Read consistency
- Fox, Gribble, Chawathe, Brewer, Gauthier:Cluster-Based Scalable Network Services(SOSP 1997,BASE 的出处)
- Dan Pritchett,《BASE: An ACID Alternative》,ACM Queue vol. 6 no. 3(2008)。ACM 站点对自动化访问返回 403,这里只给出处。
丢失订单数的算例用的是本章假设的 2,000 单/秒;三类数据的落位是 JR Academy 的归纳,文档支撑的是各参数的行为。