3. 事务与ACID能力:快照隔离、并发控制、回滚机制对比

聊到数据湖选型,事务能力是绕不开的硬骨头。我见过太多团队,把数据往湖里一丢就不管了,结果跑批的时候数据读到一半,另一个任务把文件改了——嗯,查问题查到天亮。

Iceberg 和 Delta Lake 都宣称自己支持 ACID,但实现路径完全不同。说白了,一个靠清单文件(Manifest)做快照,一个靠事务日志(Transaction Log)做版本。今天咱们就掰开揉碎,看看它们到底差在哪。

3.1 快照隔离:读不阻塞写,写不阻塞读

快照隔离是 ACID 的 I(隔离性)的核心。它保证每个读操作看到的是某个时间点的一致性快照,不受后续写入影响。

Iceberg 的快照机制

Iceberg 用 Manifest List + Manifest File 两层结构管理快照。每次提交生成一个新的 Manifest List,指向当前所有有效的数据文件。读任务拿到这个列表,就相当于拿到了一个只读快照。

核心思路:Iceberg 的快照是 不可变列表。旧快照不会被删除,只是不再被最新元数据引用。

我记得有一次做实时入湖,上游 Kafka 数据量暴增,Iceberg 的写入任务在疯狂提交新快照。下游的批处理任务读的是 5 分钟前的快照,完全不受影响。这就是快照隔离的威力——读和写各自玩各自的,互不干扰。

Delta Lake 的快照机制

Delta Lake 用 Delta Log(_delta_log 目录下的 JSON 文件)记录每次事务。每个 JSON 文件就是一个版本,读任务根据版本号重建该时间点的文件列表。

个人习惯:我倾向于把 Delta Lake 的快照理解成「增量叠加」。每次读都要从初始版本开始,把后续的 Add/Remove 操作全部重放一遍,才能得到最终文件列表。

这里有个性能坑——版本多了以后,重放开销会变大。Delta Lake 提供了 Vacuum 和 Optimize 来压缩历史,但如果你忘了做,读性能会肉眼可见地下降。

快照隔离对比表

维度 Iceberg Delta Lake
快照存储方式 Manifest List 指针 Delta Log 增量 JSON
快照读取速度 O(1) 直接定位 O(n) 重放日志
历史快照保留 默认保留,可配置 需手动 Vacuum
并发读影响 无锁,读永远成功 无锁,读永远成功

3.2 并发控制:谁先提交谁说了算

并发控制解决的是「两个人同时改同一张表怎么办」。Iceberg 和 Delta Lake 都用了乐观锁(Optimistic Concurrency Control),但实现细节不一样。

Iceberg 的乐观锁

Iceberg 依赖 Catalog(如 Hive Metastore、REST Catalog)来存储当前元数据指针。提交时,写入任务会做一次 CAS(Compare-And-Swap):

-- 伪代码逻辑
1. 读取当前元数据位置(version X)
2. 基于 version X 生成新快照
3. 尝试将元数据指针从 version X 更新到 version X+1
4. 如果失败(别人已经更新了),重试整个流程

我曾经踩过的坑:Iceberg 的 CAS 重试是「全量重试」——你要重新读取最新快照,重新规划写入文件。如果表很大,重试成本很高。所以并发写入冲突频繁的场景,Iceberg 表现不太好。

Delta Lake 的乐观锁

Delta Lake 用 文件系统级别的原子操作 做并发控制。具体来说,它依赖 S3 或 HDFS 的「文件不存在才创建」语义来写 Delta Log 文件。

-- 伪代码逻辑
1. 读取当前最新版本(version X)
2. 生成 version X+1 的 JSON 日志文件
3. 尝试原子创建 _delta_log/X+1.json
4. 如果文件已存在,说明冲突,重试

Delta Lake 的重试比 Iceberg 轻量——它只需要重新生成 JSON 日志,不需要重新规划数据文件。但前提是你的底层存储支持原子 PUT(S3 支持,HDFS 也支持)。

并发控制对比表

维度 Iceberg Delta Lake
锁机制 Catalog CAS 文件系统原子操作
冲突检测粒度 表级别 表级别
重试成本 高(全量重试) 低(仅日志重试)
高并发场景 不推荐 相对更好

3.3 回滚机制:时光倒流的能力

回滚是 ACID 的 A(原子性)和 D(持久性)的体现。数据写错了,能不能一键恢复?

Iceberg 的回滚

Iceberg 的回滚极其简单——把元数据指针指回历史快照就行。因为数据文件本身不可变,旧快照的数据文件都还在(除非被过期清理)。

-- Spark SQL 回滚到指定版本
CALL catalog.system.rollback_to_snapshot('db.table', 1234567890);

-- 或者按时间回滚
CALL catalog.system.rollback_to_timestamp('db.table', TIMESTAMP '2024-01-01 00:00:00');

我个人最喜欢 Iceberg 的一点:回滚是 O(1) 操作,只改一个指针。不管表有多大,回滚都在毫秒级完成。

Delta Lake 的回滚

Delta Lake 的回滚本质是「追加一个回滚版本」。它会生成一个新的 JSON 日志,把文件列表恢复到目标版本的状态。

-- Spark SQL 回滚到指定版本
RESTORE TABLE db.table TO VERSION AS OF 10;

-- 或者按时间回滚
RESTORE TABLE db.table TO TIMESTAMP AS OF '2024-01-01';

注意:Delta Lake 的回滚不是 O(1) 的。它需要读取目标版本的文件列表,生成新的日志条目。如果表很大,回滚操作本身就是一个写入事务,耗时取决于文件数量。

回滚机制对比表

维度 Iceberg Delta Lake
回滚方式 指针切换 追加新版本
回滚速度 毫秒级 取决于文件数量
历史版本保留 依赖过期策略 依赖 Vacuum
回滚后能否再回滚 可以(指针可来回切换) 可以(版本链完整)

3.4 核心逻辑对比图

下面这张图帮你快速理解两者的核心差异:

Iceberg vs Delta Lake 事务核心逻辑 Iceberg Manifest List CAS 指针切换 O(1) 回滚 不可变数据文件 Delta Lake Delta Log JSON 文件原子创建 追加版本回滚 增量日志重放 核心差异:Iceberg 用「指针切换」实现快照和回滚,Delta Lake 用「日志重放」 前者快但依赖 Catalog,后者慢但依赖文件系统

3.5 选型建议

说了这么多,到底怎么选?我根据自己的项目经验,给你几个判断标准:

  • 读多写少,回滚频繁:选 Iceberg。它的 O(1) 快照和回滚太香了,适合数据回溯场景。
  • 高并发写入,冲突多:选 Delta Lake。它的轻量级重试机制在高并发下更稳定。
  • 存储层不支持原子操作:选 Iceberg。Delta Lake 依赖文件系统原子 PUT,如果底层是 NFS 或老版本 HDFS,会出问题。
  • 需要细粒度时间旅行:两者都支持,但 Iceberg 的 Manifest 结构在超大规模表下性能更好。

我个人习惯是:数据湖底座用 Iceberg,因为它更开放、生态更广;但如果团队 Spark 技术栈很重,Delta Lake 的集成度更高,上手更快。没有银弹,只有权衡。