数据系统中的一致性

从关系数据库、KV到数据仓库、数据湖仓,重新梳理一致性的层次、trade-off 与研究问题

一致性的基本问题

讨论“一致性”时,经常遇到强一致、弱一致、线性一致、可串行化、最终一致、因果一致…这些词。对工程分析来说,更直接的切入方式通常是下面三个问题:

  1. 我刚写进去的数据,别人什么时候能读到?
  2. 两个并发操作同时发生时,系统的预期结果和实际结果可能是什么样的?
  3. 数据在多个系统之间流转时,下游看到的是不是同一个版本?

把这三个问题放在一起看,就会发现“一致性”并不是一个单点属性,而是三层问题:

  • 副本层:同一份数据有多个副本时,读到的是不是最新的。
  • 事务层:多个并发操作放在一起,结果还能不能解释成一个合理顺序。
  • 跨系统层:缓存、数据库、对象存储、湖仓、数仓这些系统接在一起以后,系统整体的一致性。

假设你在做一个电商系统,有三个需求,这三个需求都与一致性相关,但它们对应的并不是同一类问题:

  1. 用户下单后,页面上马上显示“已下单”。对应副本层问题:写完以后,别的读请求多久能看到。。
  2. 库存绝不能扣成负数。对应事务层问题:两个用户同时抢最后一件商品时,不能都成功。。
  3. 晚上跑报表时,订单表和库存表要对应得上。对应跨系统层问题:OLTP 库、CDC、湖仓、数仓之间是不是在看同一批数据。
💡 提示

后文将按照关系数据库、KV 系统、数据仓库 / OLAP、数据湖仓的顺序进行一致性问题的展开

Jepsen 对一致性的定义很适合作为讨论的起点:一致性模型本质上是在规定,系统允许哪些历史是“合法的”1。在工程实现中,还需要考虑系统为了维护一致性所付出的代价。

一致性定义,引用自 https://jepsen.io/consistency/models

💡 提示

因此,后面的比较采用一致性语义和成本这两个标准对不同系统的一致性进行分析。

标准1:一致性所指的语义

一致性保护了什么层面的一致性?是保护某一行,还是一个事务,或者是某张表?如果保护对象本身就不同,那么直接比较一致性并没有太多意义。 比如 Redis 很擅长做热点状态,但它默认不是拿来保护复杂事务不变量的;PostgreSQL 则反过来,最擅长的就是保护订单、支付、库存这种提交时就必须正确的事务2 3

标准2:维护一致性的成本

一致性从来不是白来的。要么你在写入时等更多副本确认。要么你在提交时做冲突检测,要么你把复杂性推给应用自己处理。

PACELC 讲的也是这个意思:即使没有网络分区,系统平时也要在延迟和一致性之间做选择4

关系数据库:优先保护事务不变量

核心目标是提交时正确

关系数据库的核心价值一直都不是“最终会收敛”,而是:

在事务提交的那一刻,数据就已经满足业务约束。

PostgreSQL 用 MVCC 提供快照读,减少读写冲突;MySQL InnoDB 用一致性读和锁来控制并发;Oracle 通过 undo 提供读一致性356

虽然三家的实现细节不同,但目标非常一致:

  • 让并发事务尽量还能讲得通;
  • 让订单、支付、库存这种核心数据在提交时就正确。

为什么订单和库存更适合关系库

还是看一个最常见的例子。

商品库存为 1。 用户 A 和用户 B 同时下单。

如果系统真的把事务做对了,最后只会有一种合理结果:

  • 要么 A 成功、B 失败;
  • 要么 B 成功、A 失败。

但不会出现:

  • 两个人都成功;
  • 库存变成负数;
  • 订单和库存扣减一边成功一边失败。

这个例子证明的是:

关系库的一致性,真正保护的是业务世界里的“不能错”。

PostgreSQL 的典型取舍

PostgreSQL 文档对 MVCC 的表述很经典:读看到的是某个时间点的快照,读不会阻塞写,写也尽量不阻塞读3。这也是 PostgreSQL 在很多业务系统里经常被作为默认选项的原因。

它不是不花代价,而是把代价放在了更复杂的并发控制和提交校验上。

MySQL 和 Oracle 的典型取舍

MySQL InnoDB 的一致性读文档里明确说了:在 REPEATABLE READ 下,同一事务内的普通读会基于第一次读建立的快照5

Oracle 则是另一种非常经典的路子:用 undo 保留旧版本,从而保证一个查询看到的是一致时间点上的已提交数据67

如果换成更直观的描述,可以理解为:

  • 查询看到的应该是一张自洽的快照;
  • 并发修改不应该把这个快照撕裂成前后不一致的结果。

KV 系统:在低延迟与正确性之间取舍

KV 系统内部差异很大,不能简单等同于“最终一致”。同样是 KV,Redis 和 HBase 的默认取舍就明显不同。

Redis:优先保护低延迟

默认复制策略的重点

Redis 默认复制是异步的。你向主节点写入成功,不代表所有副本此刻都已经追上了。WAIT 命令可以让客户端多等几个副本确认,但 Redis 官方文档也明确提醒:WAIT 只是提升数据安全性,不会把 Redis 变成一个强一致事务数据库2

这里真正需要看到的,不是“Redis 弱”,而是它做了一个非常明确的取舍:

  • 先保证写入快;
  • 再给你一个额外开关,让你按需多等几个副本;
  • 但不会承诺复杂事务语义。

为什么适合缓存,不适合作为库存主账本

下面用两个例子展开说明。

例子 1:用户登录态

用户登录后,把 session 写到 Redis。 这时候即使某个副本慢了几十毫秒,问题通常也不大。最坏情况不过是用户刚登录完,下一次请求又被要求重新登录一次。

这个例子证明的是:

如果业务允许短时间旧数据,Redis 这种“先快后补”的复制策略很划算。

例子 2:商品库存

假设商品只剩 1 件,两次请求同时进来,都在 Redis 上做扣减。Lua 脚本和原子操作可以解决一部分问题,但如果把场景扩大成“多键约束、跨表约束、故障切换后不能丢”,问题的性质就变了。

这时你要求的已经不是“一个键改成功”,而是:

  • 库存扣减和订单创建必须一起成功或一起失败;
  • 故障切换后不能出现“订单有了,库存没扣”;
  • 不能因为副本延迟读到旧值。

这个例子证明的是:

当核心目标变成“保护业务不变量”时,Redis 默认提供的能力往往不是最合适的起点。

因此,对 Redis 可以给出一个很直接的判断:

  • 如果你要的是快、简单、热点状态,它很合适。
  • 如果你要的是复杂事务正确性,你最好把主责任交给关系数据库。

HBase:优先保护行级正确性

HBase 提供了另一个很有代表性的例子,它说明 KV 系统并不只能走“最终一致”这一条路径。

默认读路径的重点

HBase 客户端里有 Consistency.STRONGConsistency.TIMELINE。官方文档的表述很明确:默认是强一致读;如果选择 TIMELINE,就可以用更低尾延迟换取可能陈旧的结果89

这说明 HBase 的思路是:

  • 默认先保证正确;
  • 如果你愿意接受旧数据,再开放更快的读路径。

这和 Redis 很不一样。Redis 是“默认先快”;HBase 更像“默认先稳”。

为什么适合大表访问,不适合作为通用事务层

HBase 的优势在于:当目标是大规模表上的行级读写,而且希望默认语义尽量稳定时,它提供了一条很清晰的路径。

例如,日志画像、宽表明细、按主键读取的大表服务,往往更关心下面两件事:

  • 单行读写默认不要太含糊;
  • 在需要时还能把读延迟继续往下压。

这个例子证明的是:

HBase 更适合“强一致行访问 + 可选旧读”的场景,而不是拿来承接通用多事务不变量。

数据仓库 / OLAP:优先保护查询快照

一致性目标与 OLTP 不同

数仓系统并不是不重视一致性,而是它们优先保护的对象与 OLTP 不同。

BigQuery 和 Snowflake 都有清晰的一致性语义,只是它们最在意的不是“每次写入立刻对所有会话可见”,而是:

  • 一次分析查询看到稳定快照;
  • 大吞吐查询不要被高冲突小事务拖垮。

BigQuery 官方文档说明它支持多语句事务,并且是 snapshot isolation;但如果你在事务里读外部数据源,底层数据变了,就不保证一致10

这很符合数仓的角色定位:仓库内部尽量提供稳定语义,跨到外部世界时,一致性边界自然会变弱。

BigQuery 和 Snowflake 的两种侧重点

BigQuery 和 Snowflake 在大方向上很接近,但默认暴露给用户的侧重点略有不同。

BigQuery 更直接地把事务和 snapshot isolation 写进文档;Snowflake 则更明确地把“跨 session 读一致性”做成一个可调参数。两者都说明了一件事:分析系统并不是不提供一致性,而是把一致性的边界和成本说得更清楚。

如果把两者放在一起看,可以得到一个更清楚的分工:

  • BigQuery 更强调事务边界和查询语义;
  • Snowflake 更强调默认读可见性与可调参数;
  • 两者都把分析吞吐放在很高的位置。

Snowflake 说明了默认值的取舍

Snowflake 文档写得很直白:

  • 表的事务隔离级别目前是 READ COMMITTED
  • 默认情况下,跨 session 的近并发变化不是总能立刻看到;
  • 如果你希望跨 session 的近并发变化也立刻可见,可以把 READ_CONSISTENCY_MODE 设成 GLOBAL,代价通常是多几毫秒延迟111213

这个例子很适合说明一件事:

一致性不是“有或没有”,而是可以被定价的。

为什么适合分析,不适合高冲突交易

假设你在数仓里跑一个日报查询,它扫很多分区、很多列、很多历史数据。你最怕的不是“某一行晚了 30 毫秒可见”,你最怕的是:

  • 查询扫到一半表版本变了;
  • 一次分析任务看到混合状态;
  • 高并发更新把整个平台卡住。

所以数仓默认更愿意把资源给:

  • 快照稳定;
  • 大吞吐;
  • 并发分析;
  • 多租户隔离。

这个例子证明的是:

OLAP 系统追求的是“分析时别看到半成品”,而不是“每次写完所有人立刻同时看到”。

数据湖仓:优先保护表快照

核心问题是快照是否完整切换

到了 Iceberg、Delta Lake 这类系统,问题就完全变了。

这里的一张表背后往往不是一份文件,而是一堆数据文件、manifest、metadata 文件。真正要保证的一致性不是“某一行是不是最新”,而是:

读者看到的,是不是同一个表版本。

这也解释了为什么底层对象存储即使已经很强,例如 S3 现在对对象读写和列表都提供强一致,也不等于系统自动拥有“表级 ACID”1415

对象存储能保证的是:

  • 某个对象写完以后能立刻读到;
  • 单个 key 的更新是原子的。

但它不能自动替你解决:

  • 多个数据文件同时生效;
  • metadata 指针如何切换;
  • 并发写者冲突怎么处理。

Iceberg 和 Delta 的基本做法

Iceberg 和 Delta 的核心套路都很像:

  1. 先把新数据文件写好。
  2. 再生成新的 metadata。
  3. 最后把“当前表版本指针”原子切到新快照。

Iceberg 官方文档把这件事讲得很清楚:它通过原子替换 metadata 文件位置,来实现 serializable isolation 和 snapshot-based reading1617。Delta Lake 也是类似思路:先读当前快照,再写新文件,最后做 validate-and-commit,如果中间有人先提交了,就冲突重试18

为什么这种设计适合分析场景

假设今天订单表有 1000 个文件,你要新增 100 个文件并删掉 30 个旧文件。

如果没有“快照切换”这一步,读者可能看到非常奇怪的中间态:

  • 新文件已经看到一半;
  • 旧文件还没删干净;
  • 一张表在两个查询里长得不一样。

而快照切换保证的是:

  • 要么你看到旧版本;
  • 要么你看到新版本;
  • 不会看到一张“拼到一半的表”。

这个例子证明的是:

湖仓系统最想保护的,不是单条写入的实时可见,而是“整张分析表始终自洽”。

为什么高并发修改更容易冲突

也正因为它把一致性放在“提交快照”这一步,所以高并发 UPDATEDELETEMERGE 时,冲突会明显增多。Delta 官方文档甚至专门列了哪些操作对会冲突18

所以湖仓很适合:

  • 批量摄取;
  • CDC 回放;
  • 大规模分析;
  • 时间旅行;
  • 多引擎共享一张表。

但如果希望直接用它替代订单主库去承接高冲突 OLTP,通常就超出了这类系统的设计重点。

把几类系统放在一起看

到这里可以把前面的判断压缩成一张对照表。

系统 最想保护什么 主要代价放在哪里 最适合的场景
PostgreSQL / MySQL / Oracle 事务提交时业务不变量成立 并发控制、锁、冲突检测、重试 订单、支付、库存、账务
Redis 这类 KV 低延迟下的可用读写 把一部分正确性压力留给应用 缓存、登录态、热点状态
HBase 这类 KV 行级强一致,必要时允许更旧的读 读路径选择和副本管理 大表、强一致行访问
BigQuery / Snowflake 查询快照稳定和高吞吐分析 跨会话可见性、外部边界、少量延迟 报表、ETL、ad-hoc 分析
Iceberg / Delta 整张表的快照一致 提交阶段的元数据切换与冲突重试 湖仓分析、CDC、批处理

如果只保留一句总结,可以记成下面这句话:

没有哪个系统天然“最一致”,只有哪个系统把一致性用在了最需要的地方。

选系统时先问什么

为业务选择系统时,通常不需要先问“要不要强一致”,而是先问下面四个问题:

问题一:更怕旧数据,还是更怕业务算错

如果你怕的是“晚几秒看到”,那通常不是最强事务问题。

如果你怕的是“多卖一件货、扣错一笔钱”,那一定先看事务层。

问题二:保护对象是一行、一笔事务,还是一整张表

缓存和 KV 往往保护键或行。 关系库保护事务。 湖仓和 OLAP 更常保护快照。

保护对象不同,工具就应该不同。

问题三:是否能够接受重试

湖仓很多时候不是“不正确”,而是“冲突了你得重试”。

如果业务很能接受批处理重试,这没有问题。 如果是用户下单按钮点一下就要马上知道成败,那体验要求就不一样。

问题四:一致性边界是否已经跨系统

这是最容易被忽略的。

很多团队单看 PostgreSQL 很强,单看 Redis 也没问题,单看数仓也很稳定。真正出问题的地方是:

  • 主库刚提交;
  • 缓存还没更新;
  • CDC 还没同步;
  • 报表已经开始读。

所以系统真正最脆弱的地方,常常不是某个单点,而是它们之间的连接处。

最后回到“强一致”这个词

单独说“这个系统强一致”,其实很容易制造误解。

更有用的说法应该是:

  • 它对什么对象强?
  • 在什么边界内强?
  • 要付出什么延迟和可用性成本?
  • 一旦跨系统,这个保证还剩多少?

只有把这几个问题一起说清楚,“一致性”才不是一个空词。

如果把全文压缩成一个结论,可以写成这样:

一致性不是一个单一指标,而是系统对“读到什么、按什么顺序发生、在哪个边界内成立”所做的一组承诺。工程上真正重要的,不是盲目追求最强,而是先看清你要保护的对象,再决定把代价花在哪里。


  1. Jepsen, “Consistency Models”. https://jepsen.io/consistency/models ↩︎

  2. Redis documentation, WAIT command. https://redis.io/docs/latest/commands/wait/ ↩︎ ↩︎

  3. PostgreSQL documentation, MVCC / concurrency control. https://www.postgresql.org/docs/current/mvcc.html ↩︎ ↩︎ ↩︎

  4. Daniel J. Abadi, “Consistency Tradeoffs in Modern Distributed Database System Design: CAP is Only Part of the Story”. https://www.cs.umd.edu/~abadi/papers/abadi-pacelc.pdf ↩︎

  5. MySQL documentation, InnoDB consistent nonlocking reads. https://dev.mysql.com/doc/refman/8.1/en/innodb-consistent-read.html ↩︎ ↩︎

  6. Oracle Database Concepts, data concurrency and consistency. https://docs.oracle.com/en/database/oracle/oracle-database/19/cncpt/data-concurrency-and-consistency.html ↩︎ ↩︎

  7. Oracle documentation, undo and read consistency. https://docs.oracle.com/html/E25494_01/undo001.htm ↩︎

  8. Apache HBase API, Consistency enum. https://hbase.apache.org/2.3/apidocs/org/apache/hadoop/hbase/client/Consistency.html ↩︎

  9. Apache HBase Reference Guide, timeline consistency. https://hbase.apache.org/1.1/book.html#timeline.consistency ↩︎

  10. BigQuery documentation, multi-statement transactions. https://cloud.google.com/bigquery/docs/transactions ↩︎

  11. Snowflake documentation, transactions and read consistency across sessions. https://docs.snowflake.com/en/sql-reference/transactions ↩︎

  12. Snowflake documentation, READ_CONSISTENCY_MODE parameter. https://docs.snowflake.com/en/sql-reference/parameters ↩︎

  13. Snowflake documentation, LOCK_WAIT_HISTORY view. https://docs.snowflake.com/en/sql-reference/account-usage/lock_wait_history ↩︎

  14. Amazon S3 User Guide, data consistency model. https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html ↩︎

  15. AWS News Blog, “Amazon S3 Update – Strong Read-After-Write Consistency”. https://aws.amazon.com/blogs/aws/amazon-s3-update-strong-read-after-write-consistency/ ↩︎

  16. Apache Iceberg documentation, reliability. https://iceberg.apache.org/docs/1.9.0/reliability/ ↩︎

  17. Apache Iceberg specification. https://apache.github.io/iceberg/spec/ ↩︎

  18. Delta Lake documentation, concurrency control. https://docs.delta.io/latest/concurrency-control.html ↩︎ ↩︎