2. Lakehouse核心概念:ACID事务、Schema-on-Read与Schema-on-Write、开放存储格式

好,咱们正式开始聊 Lakehouse 的核心概念。说实话,这些概念单独拎出来都不难理解,但把它们组合在一起,才是 Lakehouse 真正能打的地方。我最早接触这些概念时,也觉得不就是把数仓和数湖拼一块吗?后来踩了几个坑才明白——拼凑不是架构,融合才是。

这一节,我带你拆开三个最关键的基石:ACID 事务、Schema 策略、以及开放存储格式。理解了它们,你就理解了 Lakehouse 为什么能同时扛住批处理和实时流。

2.1 ACID 事务:让数据湖不再「脏乱差」

传统数据湖有个老毛病——写了一半,读出来是脏数据。你想想看,一个 Spark 任务正在往 Parquet 目录写文件,另一个查询刚好读到那个目录,结果就是文件不完整、数据对不上。我早期做离线数仓时,经常半夜被叫起来排查「数据对不上」的问题,最后发现都是并发写入惹的祸。

Lakehouse 引入 ACID 事务,说白了就是给数据湖装上了数据库级别的原子性、一致性、隔离性、持久性。这不是什么新概念,但放在分布式文件系统上实现,难度不小。

核心要点:ACID 事务让多个写入操作要么全部成功,要么全部回滚。读操作永远看到的是已提交的完整快照。

具体怎么实现的?以 Delta Lake 为例,它用了一个 事务日志(Transaction Log) 来记录每次变更。每次写入,先写日志,再更新数据文件。读的时候,只读日志里标记为「已提交」的版本。这样并发读写就不会互相干扰了。

我的经验:有一次做实时流写入,上游 Kafka 数据偶尔乱序,导致 Delta 表出现重复记录。后来我开了 mergeSchema 配合 optimizeWrite,再用 vacuum 清理旧版本,才彻底解决。记住:ACID 不是银弹,你得配合合理的 compaction 策略。

2.2 Schema-on-Read vs Schema-on-Write:两种哲学

这两个词听起来有点绕,其实很简单:

  • Schema-on-Write:写入时就定义好结构,不符合格式的数据直接拒掉。传统数仓就是这么干的。
  • Schema-on-Read:写入时不检查结构,读的时候再解析。传统数据湖(比如 Hive)就是这么玩的。

你可能会问:哪个更好?嗯,这得看场景。

对比维度 Schema-on-Write Schema-on-Read
写入性能 较慢(需要校验) 快(无校验)
读取性能 快(结构已知) 较慢(需要解析)
灵活性 低(结构固定) 高(可适应变化)
数据质量 高(写入即校验) 低(依赖读时处理)
典型场景 金融、监管报表 日志、原始数据探索

Lakehouse 的聪明之处在于——它两者都支持。你可以在写入时用 Schema-on-Write 保证质量,在读取时用 Schema-on-Read 灵活扩展。比如 Delta Lake 的 schema enforcement 默认开启,但你也可以关掉它,让数据先进来再说。

避坑指南:我曾经在一个实时数仓项目里,为了图省事,把所有表都设成了 Schema-on-Read。结果上游改了字段类型,下游报表直接崩了。后来我改成:核心业务表用 Schema-on-Write,探索性表用 Schema-on-Read。这个原则一直用到现在。

2.3 开放存储格式:Delta Lake / Iceberg / Hudi

聊完概念,咱们看看具体实现。目前主流的开放存储格式有三个:Delta Lake、Apache Iceberg、Apache Hudi。它们都支持 ACID、Schema 演化、时间旅行,但侧重点不同。

2.3.1 Delta Lake:Databricks 的亲儿子

Delta Lake 是 Databricks 开源的,跟 Spark 集成最紧密。它的核心是 事务日志 + Parquet 文件。每次操作都写一条 JSON 日志,记录哪个文件被修改了。

-- Delta Lake 示例:创建表并插入数据
CREATE TABLE events (
  eventId STRING,
  eventTime TIMESTAMP,
  eventType STRING
) USING DELTA;

INSERT INTO events VALUES
  ('e1', '2024-01-01 10:00:00', 'click'),
  ('e2', '2024-01-01 10:01:00', 'view');

-- 查看历史版本
DESCRIBE HISTORY events;

我个人习惯用 Delta Lake 做实时流处理,因为它的 foreachBatch 配合 merge 操作,处理 CDC 数据特别顺手。

2.3.2 Apache Iceberg:开放生态的集大成者

Iceberg 由 Netflix 和 Airbnb 贡献,后来捐给了 Apache。它的设计更「数据库化」——用 元数据树 来管理文件清单。支持 Trino、Flink、Spark、Hive 等多种引擎。

-- Iceberg 示例:创建表并分区
CREATE TABLE orders (
  orderId BIGINT,
  customerId BIGINT,
  amount DECIMAL(10,2),
  orderDate DATE
) USING ICEBERG
PARTITIONED BY (orderDate);

-- 时间旅行查询
SELECT * FROM orders FOR SYSTEM_VERSION AS OF 1234567890;

我记得有一次做跨引擎查询,Spark 写数据,Trino 读数据,Iceberg 是唯一不需要额外配置就能跑通的格式。它的 hidden partitioning 特性也很赞——你不需要手动指定分区字段,Iceberg 自动帮你优化。

2.3.3 Apache Hudi:增量处理的先行者

Hudi 由 Uber 开源,最早就是为了解决「如何高效处理增量数据」这个问题。它引入了 Copy-on-Write 和 Merge-on-Read 两种存储类型。

-- Hudi 示例:创建 MOR 表
CREATE TABLE rides (
  rideId BIGINT,
  driverId BIGINT,
  fare DECIMAL(10,2)
) USING HUDI
TBLPROPERTIES (
  'hoodie.table.type' = 'MERGE_ON_READ',
  'hoodie.datasource.write.recordkey.field' = 'rideId'
);

-- 增量查询
SELECT * FROM rides WHERE _hoodie_commit_time > '20240101000000';

如果你做的是 近实时 OLAP,Hudi 的增量查询能力很强。但要注意,Merge-on-Read 表的读性能会差一些,需要定期做 compaction。

2.4 三种格式对比:怎么选?

特性 Delta Lake Apache Iceberg Apache Hudi
事务实现 事务日志(JSON) 元数据树(Avro) 时间线(Timeline)
Schema 演化 支持(自动合并) 支持(全类型) 支持(有限制)
时间旅行 支持(版本号/时间戳) 支持(快照ID/时间戳) 支持(提交时间)
增量查询 需 CDC 配合 需增量读取 API 原生支持
引擎兼容性 Spark 为主 Spark/Flink/Trino/Hive Spark/Flink
实时写入 好(微批 + merge) 好(Flink 集成) 优秀(流式 upsert)

我的选型建议:

  • 如果你团队 Spark 技术栈强,选 Delta Lake,上手快、坑少。
  • 如果你需要多引擎查询(Trino + Spark + Flink),选 Iceberg,生态最开放。
  • 如果你核心场景是增量 CDC 或近实时 OLAP,选 Hudi,增量处理是它的看家本领。

2.5 核心概念关系图

下面这张图,帮你理清这三个概念之间的关系:

Lakehouse 核心概念关系图 Lakehouse 核心概念关系 Lakehouse 架构 ACID 事务 Schema 策略 开放存储格式 原子性 一致性 隔离性 持久性 Schema-on-Write Schema-on-Read Delta Lake Iceberg Hudi 💡 三者关系: ACID 事务是「规则」,Schema 策略是「方式」,开放存储格式是「载体」。三者缺一不可。

从图上你能看到:ACID 事务 是 Lakehouse 的「规则引擎」,保证数据一致性;Schema 策略 是「数据契约」,决定数据怎么进、怎么出;开放存储格式 是「物理载体」,把规则和契约落地到文件系统上。

嗯,这一节内容不少,但都是后面章节的基础。你先把这三个概念吃透,后面聊实时流处理、CDC、增量查询时,你会发现它们无处不在。