2、实时湖仓基础:数据湖与数据仓库的区别、实时数仓架构、常见实时湖仓技术栈

2.1 数据湖与数据仓库:从“先建模”到“先存储”的范式转变

在理解实时湖仓之前,必须先厘清数据湖与数据仓库的本质差异。传统数据仓库(Data Warehouse)遵循 Schema-on-Write 原则:数据在写入之前必须完成建模、清洗和转换,以保障查询性能与数据一致性。这种模式适合业务稳定、指标明确的场景,但面对半结构化日志、IoT 流数据或快速演变的业务字段时,往往需要漫长的 ETL 开发周期。

数据湖(Data Lake)则采用 Schema-on-Read 策略:原始数据以原生格式(Parquet、ORC、JSON 等)直接存储,不预先定义强约束。这种设计带来了极大的存储灵活性和低成本优势,但也引发了“数据沼泽”问题——缺乏事务保障、难以执行 ACID 更新、文件级一致性无法保证,导致多个作业并发读写同一张表时极易产生数据冲突。

下表从关键维度对比两者的核心差异:

对比维度 数据仓库 数据湖 湖仓一体(Lakehouse)
数据写入模式 Schema-on-Write(先建模后写入) Schema-on-Read(先存储后解析) Schema-on-Write + 后期灵活演进
事务支持 完整 ACID 无或极弱 表级 ACID(通过时间旅行与快照隔离)
存储成本 高(需预处理与压缩) 低(原始格式直接落盘) 中(列式存储 + 索引优化)
数据新鲜度 批处理为主,T+1 常见 可存储实时数据,但难以查询 流批一体,分钟级甚至秒级可见
适用场景 固定报表、KPI 分析 数据探索、机器学习原始数据集 实时报表 + 增量计算 + 数据科学

实时湖仓(Real-time Lakehouse)正是为了弥合上述鸿沟而出现:它保留了数据湖的低成本存储与开放性,同时引入数据仓库的事务能力、索引优化和约束管理。其核心机制是在文件系统之上增加一层事务日志(Transaction Log),用于记录每次写入的快照、文件变更和提交顺序,从而让多个流作业或批作业可以并发安全地读写同一张表。

传统数据仓库 Schema-on-Write ETL 清洗 / 转换 强约束表结构 ACID 事务 T+1 批查询 数据湖 Schema-on-Read 原始格式存储 无预定义 Schema 无事务保障 并发写易冲突 湖仓一体 事务日志 + 快照隔离 流批写入统一 ACID 表级事务 时间旅行 / 版本回滚 分钟级实时可见 演进 融合 核心差异:数据写入时是否定义 Schema、是否提供事务日志、是否支持并发安全更新

2.2 实时数仓架构:从 Lambda 到 Kappa 再到湖仓流批一体

实时数仓的架构演进经历了三个阶段,理解这一脉络有助于定位合并冲突产生的根源。

Lambda 架构是最早的实时数仓方案,它同时维护批处理层(Batch Layer)和速度层(Speed Layer)。批处理层负责全量数据的精确计算,速度层负责实时数据的近似计算,最终在服务层合并结果。这种架构的致命缺陷在于同一份业务逻辑需要维护两套代码,且批流结果合并时经常出现数据口径不一致,为后续的数据冲突埋下隐患。

Kappa 架构则尝试统一处理链路:所有数据以流的方式进入消息队列(如 Kafka),通过流计算引擎(如 Flink)进行实时处理,需要重算时则从 Kafka 中回放历史数据。Kappa 架构简化了运维,但受限于消息队列的保留时长,无法高效处理大规模历史数据回溯,且流计算状态管理在长时间窗口下容易产生状态不一致。

当前主流的湖仓流批一体架构将 Kafka 作为实时数据入口,Flink 负责流式 ETL 与实时计算,计算结果直接写入湖仓表(Hudi/Iceberg/Delta Lake)。同时,批处理作业(如 Spark SQL)可以直接读取同一张湖仓表进行全量分析,无需维护两套存储。这种架构的关键在于:

  • 统一存储层:流作业与批作业共享同一份数据文件,通过事务日志协调并发。
  • 增量与全量共存:流作业持续写入增量数据,批作业可读取历史快照,两者通过快照隔离互不阻塞。
  • 实时数仓分层:ODS(操作数据层)直接对接 Kafka 原始流,DWD(明细层)进行清洗与维度补充,DWS(汇总层)进行轻聚合,ADS(应用层)服务报表与告警。
-- 典型实时数仓链路(Flink SQL + Hudi)
-- 1. 从 Kafka 读取原始订单流
CREATE TABLE kafka_orders (
  order_id BIGINT,
  user_id BIGINT,
  product_id BIGINT,
  amount DECIMAL(10,2),
  ts TIMESTAMP(3),
  WATERMARK FOR ts AS ts - INTERVAL '5' SECOND
) WITH ('connector' = 'kafka', 'topic' = 'ods_orders', ...);

-- 2. 写入 Hudi 明细表(DWD 层)
CREATE TABLE hudi_orders (
  order_id BIGINT,
  user_id BIGINT,
  product_id BIGINT,
  amount DECIMAL(10,2),
  ts TIMESTAMP(3),
  PRIMARY KEY(order_id) NOT ENFORCED
) WITH ('connector' = 'hudi', 'table.type' = 'MERGE_ON_READ', ...);

INSERT INTO hudi_orders SELECT * FROM kafka_orders;

2.3 三大实时湖仓技术栈:Hudi、Iceberg、Delta Lake 核心机制

当前主流的实时湖仓技术栈均采用“文件 + 事务日志”的抽象,但实现细节差异显著,这些差异直接决定了合并冲突的表现形式与处理策略。

Apache Hudi 的核心抽象是 Copy-on-Write(COW) 与 Merge-on-Read(MOR) 两种表类型。COW 在每次写入时直接重写受影响的数据文件,读取性能好但写入放大明显;MOR 将更新记录先写入增量日志文件(Avro 格式),读取时再合并基础文件与日志文件,适合写多读少的场景。Hudi 使用 文件组(File Group) 和 文件切片(File Slice) 管理数据版本,每个文件组包含多个文件切片,同一文件组内不同切片代表不同时间点的数据版本。

Apache Iceberg 则采用 快照(Snapshot) 隔离机制,每次写入生成一个新的快照,快照之间通过元数据树(Metadata Tree)关联。Iceberg 的元数据层包含三层:Catalog(指向当前元数据指针)、Metadata File(记录表结构、分区信息与快照列表)、Manifest List 与 Manifest File(描述数据文件与删除文件的分布)。Iceberg 的删除文件(Delete File)机制支持位置删除与等值删除,在合并数据时需同时处理数据文件与删除文件,这一过程是合并冲突的高发区。

Delta Lake 基于 事务日志(Transaction Log) 实现 ACID,日志以 JSON 格式存储在 _delta_log 目录下。每次写入操作(INSERT、UPDATE、MERGE)都会生成一个新的日志条目,记录新增或删除的数据文件。Delta Lake 的 MERGE 操作(即 Upsert)在并发执行时,如果两个事务同时修改同一数据文件,后提交的事务会因文件被修改而失败,需要重试。

技术栈 事务日志机制 更新方式 并发冲突典型场景
Apache Hudi Timeline(时间线)记录 commit / rollback COW 重写文件 / MOR 追加日志 两个流作业同时 upsert 同一文件组
Apache Iceberg Snapshot 快照链 + Manifest 列表 数据文件 + Delete File 标记 并发提交时元数据指针竞争
Delta Lake _delta_log 事务日志(JSON) 重写 Parquet 文件 + 日志追加 MERGE 操作与流式写入冲突

2.4 合并冲突的根源:从技术栈机制看问题本质

合并冲突(Merge Conflict)并非某个技术栈的缺陷,而是分布式并发写入同一张表时,事务隔离机制无法自动协调文件级修改的必然结果。以 Hudi MOR 表为例,当两个 Flink 作业同时向同一文件组写入更新记录时,它们各自生成独立的日志文件,但元数据时间线只能记录一个提交为“成功”,另一个提交会因文件版本不匹配而失败。

更隐蔽的冲突发生在 Iceberg 的删除文件与数据文件合并阶段。假设作业 A 读取了快照 S1 中的数据文件 F1,并准备将某些行标记为删除;与此同时,作业 B 也基于 S1 对 F1 进行了更新并提交,生成了新快照 S2。当作业 A 尝试提交时,Iceberg 发现 F1 已不在 S2 的当前文件中,于是抛出 CommitFailedException。这种冲突并非数据内容矛盾,而是基于过期快照的写入导致的。

Delta Lake 的冲突则更多体现在 MERGE 操作的“读-写-写”竞态上。一个流作业持续向表中追加数据,同时一个批处理作业执行 MERGE INTO 更新历史记录。如果批处理作业读取了某个数据文件,而流作业恰好在该文件末尾追加了新行,批处理作业提交时就会因文件被修改而失败。

常见误区:许多开发者认为合并冲突是“数据内容重复”或“主键冲突”,实际上绝大多数冲突是文件版本冲突——即两个事务基于同一文件版本进行修改,后提交者无法识别前者的变更。因此,解决冲突的关键不是去重,而是控制并发写入粒度、优化事务重试策略、合理设计分区与主键。

理解上述机制后,我们可以总结出合并冲突的三大根因类别:

  1. 并发写同一文件组:多个流作业或流批混合作业同时更新相同范围的数据文件。
  2. 快照隔离失效:长事务读取旧快照,提交时目标文件已被其他事务修改。
  3. 元数据竞争:多个作业同时尝试更新表的元数据指针(如 Iceberg 的 Catalog 更新、Hudi 的 Timeline 写入)。