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 核心概念关系图
下面这张图,帮你理清这三个概念之间的关系:
从图上你能看到:ACID 事务 是 Lakehouse 的「规则引擎」,保证数据一致性;Schema 策略 是「数据契约」,决定数据怎么进、怎么出;开放存储格式 是「物理载体」,把规则和契约落地到文件系统上。
嗯,这一节内容不少,但都是后面章节的基础。你先把这三个概念吃透,后面聊实时流处理、CDC、增量查询时,你会发现它们无处不在。