想象你经营着一间巨大的公共图书馆。书库里有几万个书架,读者随时进来借阅,图书管理员负责把新书放上书架、把旧书替换掉。一开始图书馆管理很粗放:谁想改书,就直接把书架上的那本抽出来涂改,改完放回去。于是问题来了——一个读者正捧着某本书在读,另一个管理员却把这本书改了;两个管理员同时想改同一本书,后改的人把先改的人覆盖掉了;甚至有人改到一半突然停电,书架上留下一本缺了十几页的残书。 这就是数据湖最初的样子:便宜、能装海量数据的对象存储,像极了那个管理混乱的大书库。文件只能整体覆盖、没有并发保护、改到一半崩溃就会留下半成品。后来,Iceberg、Delta Lake 和 Hudi 这三个「图书管理公司」出现了。它们不换书架、不动书库,而是在书架旁加了一套严格的馆员制度——ACID 事务支持。从此,读者看到的是固定版本的馆藏,写者改书不会互相踩踏,改到一半断电也能安全回滚。这本书,就讲这套制度是怎么让数据湖变得可信的。

岗位关联

掌握数据湖 ACID 机制,是成为大数据架构师和云数据平台工程师的核心加分项——它是数据湖从「能存」走向「靠谱」的关键分水岭。

目录
  1. 可视化演示
  2. 事务的由来
  3. 快照隔离
  4. Copy-on-Write
  5. 乐观并发控制
  6. 三大系统实现
  7. 时间旅行
  8. 写入模型对比
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

事务的由来

回到那间大书库。为什么书库一开始乱?因为书架本身是「只会放书、不会改书」的:要更新一本书,只能把整本书从架上抽走,放一本全新的上去。如果两个管理员同时操作,后放的会把先放的盖掉;如果抽到一半停电,书架上留下的是残书。对象存储(Object Storage)正是这样的书架——它只支持整体写入和覆盖,不支持原地修改、也不提供并发保护。数据湖把数据放在对象存储上,等于把成千上万本书直接摞在开放式书架上,却没有馆员管理。

后来人们发现,数据仓库早就解决了这个问题——它像一个有严格馆员制度的封闭阅览室,但容量有限、价格昂贵。数据湖想要的是「仓库的规矩 + 书库的容量」。于是事务层出现了:它不改变书架本身,而是在书架旁增加一套管理逻辑,让数据湖拥有数据库才有的 ACID 保证——原子性、一致性、隔离性、持久性。这三套方案就是 Iceberg、Delta Lake 和 Hudi。

在没有任何保护的书库里,读者和管理员频繁交互,更新丢失的风险与读写操作的数量密切相关。我们可以用一个简化的模型表达这种风险:

没有馆员的书库,风险与操作频率成正比。ACID 事务层就是那套「馆员制度」——它不换书架,却让整个书库有了秩序。理解了为什么需要它,接下来看它最核心的手段:让读者先「冻结」眼前的书架。

Plost=R×WTP_{\mathrm{lost}} = \frac{R \times W}{T}
一段时间内,读操作越多、写操作越多,数据被覆盖破坏的概率就越高;事务层做的第一件事,就是把这个概率压到趋近于零。
  1. $R$(读操作次数)在一段时间内,读者翻阅、查询数据的次数。这个值越大,意味着越多的「眼睛」正盯着可能被改动的数据。
    RR
  2. $W$(写操作次数)同一段时间内,写者修改、覆盖数据的次数。写操作是破坏风险的直接来源。
    WW
  3. $T$(时间窗口)统计这些操作的时间跨度,把风险归一化到单位时间,便于横向比较。
    TT
  4. $P_{\mathrm{lost}}$(更新丢失概率)读与写在同一窗口内互相干扰的可能性,代表数据不一致的隐患程度。
    PlostP_{\mathrm{lost}}

快照隔离

读者走进书库,管理员递给他一张印着编号的「快照卡」,上面写着:v1。接下来的半小时里,无论书架上发生什么变化——有人换书、有人加书、有人删书——这位读者看到的始终是 v1 编号对应的馆藏状态。其他读者拿到的是 v2、v3 的卡片,各看各的,互不干扰。这就是快照隔离(Snapshot Isolation)的直观感受:每个读者拥有一个「冻结时刻」的数据视图。

实现起来并不玄妙。事务层维护一份「版本指针」,指向当前有效的数据文件清单。每次提交事务,就生成一个新的清单并切换指针,旧的清单不删除。读者开始查询时,事务层把当时的指针「钉」在它的会话上,之后所有读取都沿着这个指针找文件。因为文件是不可变的、写后不覆盖,所以读者读到的内容天然稳定,甚至不需要加任何读锁。

数据湖的元数据层记录了所有版本间的先后关系。一个快照要么等于当前最新版本,要么早于它,绝不可能「晚于」最新版本——否则就出现了穿越时空的幻觉。

快照隔离让读者彻底安心:书架再怎么变,我手里的卡片决定我看到的世界。那么写者呢?他们怎么在不破坏旧书的前提下完成修改?答案是:别涂改原书,抄一本新的。

Ssnapshot⪯SlatestS_{\mathrm{snapshot}} \preceq S_{\mathrm{latest}}
快照版本永远不晚于最新版本,这是快照隔离合法性的数学表达。
  1. $S_{\mathrm{latest}}$(最新版本号)当前书库里最新一次事务提交后的版本编号,代表最新的馆藏状态。
    SlatestS_{\mathrm{latest}}
  2. $S_{\mathrm{snapshot}}$(快照版本号)读者进入阅览室那一刻被分配的版本编号,之后它的读取固定指向这个编号。
    SsnapshotS_{\mathrm{snapshot}}
  3. $\preceq$(偏序关系)读作「不晚于」,表示版本之间存在先后顺序,且不要求任意两个版本都可比较。
    ⪯\preceq
  4. 整体含义读者手里的快照版本,要么是当前最新版,要么是历史某个旧版本,绝不可能超前。这保证快照视图是合理且自洽的。
    Ssnapshot⪯SlatestS_{\mathrm{snapshot}} \preceq S_{\mathrm{latest}}

Copy-on-Write

你是一个图书管理员,接到任务把《数据湖入门》第 3 章更新一下。按照老办法,你得从书架上抽出那本书,直接涂改——但这会破坏正捧着 v1 快照卡阅读的读者。事务层给出的规则简单粗暴:不许涂改原书,抄一本新的再改。你复制一本《数据湖入门》,在副本上完成第 3 章的修订,然后把它放上书架,签上新的版本号 v2。

这就是 Copy-on-Write(写时复制,简称 COW)机制。每次更新数据时,事务层把需要修改的数据文件复制一份,在副本上写入变更,提交时用新文件替换元数据清单中的旧条目。旧文件原封不动地留在存储里,仍然被 v1 快照的清单引用。读取路径因此变得极其简单:按清单指针找文件,拿到的永远是完整、一致的数据文件,不需要现场拼接。

代价是「写放大」——明明只改了一点点数据,却要把整个文件复制一遍。我们把写入放大系数定义为:

Copy-on-Write 让「改书」和「读书」彻底解耦:读者安心读旧版,写者放心写副本,互不侵犯。但问题来了——两个管理员同时抄同一本书怎么办?这就轮到乐观并发控制登场。

WA=R+NN\mathrm{WA} = \frac{R + N}{N}
写放大系数衡量写路径的额外开销:重写字节数越多、真正的新增数据越少,系数越大,成本越高。
  1. $N$(新增数据字节)这笔事务真正想写入的新数据量,比如只更新了 1 个字段对应的 100 字节。
    NN
  2. $R$(重写的旧数据字节)因为复制旧文件而额外写入存储的数据量。文件越大,R 越惊人。
    RR
  3. $\mathrm{WA}$(写放大系数)实际写入存储的总字节与新增有效字节的比值,反映存储层承受的额外负担。
    WA\mathrm{WA}
  4. 整体关系分子是总写入量,分母是有用写入量。系数等于 1 代表零浪费,系数越大代表复制开销越高。
    WA=R+NN\mathrm{WA} = \frac{R + N}{N}

乐观并发控制

两个管理员同时看上了《数据湖入门》,都想在第 3 章加一段新内容。悲观的做法是:给这本书上把锁,谁先拿到谁改,另一个排队等。数据湖的选择恰恰相反——乐观:你们各抄各的,尽管改。等要提交的时候,馆员问一句:「你开始抄的时候,原书是第几版?」如果两个人的答案都是 v1,而对方已经在 v1 的副本上提交了 v2,那后提交的人就会被告知:你已经基于过期版本修改了,请重新基于 v2 再来一遍。

乐观并发控制(Optimistic Concurrency Control)的核心思想是:写入时不加锁,提交时校验版本。事务引擎为每次事务记录它读取的基础版本号,提交时检查这个版本号是否仍然等于当前的元数据版本。提交成功的判定条件可以写作:

乐观并发控制特别适合数据湖的典型场景:读多写少。大多数时候提交都能一次通过,省去了持锁的开销。只有当两个写者真的撞车时,才需要有人退让一步。而在实现层面,Iceberg、Delta Lake 和 Hudi 正是把这套乐观逻辑落地成产品的三家公司。

Vcommit=VbaseV_{\mathrm{commit}} = V_{\mathrm{base}}
提交时携带的版本号等于开始计算时读取的基础版本号,这笔事务才被允许落盘。
  1. $V_{\mathrm{base}}$(基础版本号)事务开始执行时,写者基于哪个版本的数据进行了计算。这个版本号被记录在事务上下文中。
    VbaseV_{\mathrm{base}}
  2. $V_{\mathrm{commit}}$(提交版本号)事务准备提交时,从元数据层读到的当前最新版本号。
    VcommitV_{\mathrm{commit}}
  3. 等号判定两者相等,说明准备期间没人抢先提交,本次修改安全,允许落盘;不等则证明有并发冲突。
    Vcommit=VbaseV_{\mathrm{commit}} = V_{\mathrm{base}}
  4. 冲突处理校验失败后,事务自动放弃本次写入,基于最新版本重新读取数据、重新计算、再次提交,直到成功或达到重试上限。
    Vcommit≠Vbase⇒retryV_{\mathrm{commit}} \neq V_{\mathrm{base}} \Rightarrow \text{retry}

三大系统实现

现在有三家图书管理公司来竞标你的书库改造项目,它们分别是 Iceberg、Delta Lake 和 Hudi。三家公司都承诺提供 ACID 事务,但管理风格各有侧重。Iceberg 是一个「表格式」(Table Format)标准,它最激进地拥抱开放生态:不绑定任何计算引擎,Spark、Flink、Trino 都能用它管理数据,像一位中立的技术顾问,谁家引擎都能配合。它的元数据采用分层树状结构,快照管理非常高效。

Delta Lake 则深度绑定 Spark 生态,由 Databricks 公司主导。它的核心是一份可追加的事务日志(Transaction Log):每次提交就往日志里追加一条 JSON 记录,所有表状态都可以从这份日志里重建,像图书馆里那本永远只增不删的「馆务台账」。对于已经在用 Spark 的团队,Delta 的上手成本最低。Hudi 则把重心放在增量处理上,它最早为 Uber 的轨迹数据而生,擅长流式摄取和 Upsert(有则更新、无则插入),像一位专攻「物流入库」的运营专家。

三家的取舍,本质上是在读写成本之间做权衡。用一个简化模型来表达选型哲学:

三大公司各有所长,但它们的共同点是都保留了旧版本的数据文件。这正是我们接下来要介绍的隐藏宝藏——时间旅行。

Ctotal=α⋅Cwrite+β⋅CreadC_{\mathrm{total}} = \alpha \cdot C_{\mathrm{write}} + \beta \cdot C_{\mathrm{read}}
总成本等于写入成本与读取成本的加权和;写密集的场景应多考虑写入友好的方案(如 Merge-on-Read),读密集场景则应优先读取性能。
  1. $C_{\mathrm{write}}$(写入成本)每次事务写入在存储和计算上的开销,包括复制文件、写日志、合并小文件等。
    CwriteC_{\mathrm{write}}
  2. $C_{\mathrm{read}}$(读取成本)查询数据时扫描、合并、解析文件的开销,直接决定报表和机器学习任务的响应速度。
    CreadC_{\mathrm{read}}
  3. $\alpha$、$\beta$(权重系数)表达业务对读写性能的偏好。流式摄入场景 $\alpha$ 大,交互式分析场景 $\beta$ 大。
    α, β\alpha,\ \beta
  4. $C_{\mathrm{total}}$(总成本)读写成本加权求和,选型时应取最小化这个值的方案。
    CtotalC_{\mathrm{total}}

时间旅行

书库保留了每一版书籍的完整副本,这听起来很浪费,但带来了一个神奇的能力:时间旅行。读者可以拿着 v1 的快照卡,要求查阅三个星期前的馆藏状态;管理员出了错,可以把整个书库回滚到昨天下午的版本;审计人员想知道《数据湖入门》一年来改过多少次,每次改了什么,翻快照记录就知道。数据湖的三大系统都支持这种能力——本质上是因为旧文件没有被删除,旧清单仍然指向它们。

时间旅行的技术实现,靠的是「清单文件」(Manifest)。每个快照版本都对应一组清单,清单里列出该版本包含的所有数据文件。查询某个历史版本时,引擎不再解析最新清单,而是解析该版本对应的旧清单,然后沿着清单扫描数据文件:

时间旅行让数据湖有了「记忆」,但也提醒我们:快照不会永远保留,存储成本会迫使管理员定期清理过期版本。最后,我们把目光投向流水线——当数据不是一批批到达,而是一条条持续涌来时,事务层如何应对?

Q(v)=Scan(Manifestv)Q(v) = \mathrm{Scan}(\mathrm{Manifest}_v)
对版本 v 的查询,等于扫描版本 v 对应的文件清单;清单在,历史就在。
  1. $v$(目标版本号)你想回到哪个历史时刻,比如 “2024-03-15 的快照”。
    vv
  2. $\mathrm{Manifest}_v$(版本清单)记录版本 v 包含哪些数据文件的元数据列表,是快照的「藏书目录」。
    Manifestv\mathrm{Manifest}_v
  3. $\mathrm{Scan}$(扫描操作)按清单逐个读取文件并执行查询计算的过程。
    Scan\mathrm{Scan}
  4. $Q(v)$(版本查询)整个时间旅行的查询结果——只要清单未被清理,任何历史版本都可被重建。
    Q(v)Q(v)

写入模型对比

真实的数据世界里,数据不是一天运一车来的,而是像水管里的水一样持续流动:点击流、订单流、物联网日志,每秒都有新记录涌入。图书馆的比喻要升级了——这不再是管理员定期批量上书,而是一条加印流水线:不断有新版小册子从印刷机下来,需要立刻上架。这时,Copy-on-Write 的「复制一整本再改」就显得笨重:每个新事件都要复制文件,写放大和延迟都难以接受。

于是出现了 Merge-on-Read(读时合并) 模式:新到的数据先写成小体积的增量文件(Delta 文件),不打扰已有的基础文件;查询时再把基础文件和增量文件实时合并起来。代价是读取变慢——查询引擎要现场做「拼书」工作。Hudi 对这种模式支持得最成熟,非常适合高吞吐、频繁小批量写入的场景。而 Copy-on-Write 则仍然占据「读多写少」的主场。

无论哪种模式,事务层都让数据湖拥有了真正的 Upsert 语义:按主键判断,有则更新、无则插入,这直接支撑了流式更新——数据以分钟级延迟进入湖中,查询端看到的数据新鲜度可以用下面的差值来刻画:

从批量写入到流式更新,事务层让数据湖真正「动」了起来。回看整趟旅程:快照隔离给了读者固定视角,Copy-on-Write 给了写者安全操作的空间,乐观并发控制了结冲突,三大系统落地实现,时间旅行创造历史,写入模型对比指引选型——ACID 事务支持,就是让数据湖从「能存」到「靠谱」的那套馆员制度。

---

Rfreshness=tnow−tingestR_{\mathrm{freshness}} = t_{\mathrm{now}} - t_{\mathrm{ingest}}
数据新鲜度等于当前时刻与最近一笔事务落盘时刻的间隔;间隔越小,湖里的数据越「鲜活」。
  1. $t_{\mathrm{ingest}}$(数据落盘时刻)最新一笔事务提交到事务层的时间,数据在此刻成为可查询的版本。
    tingestt_{\mathrm{ingest}}
  2. $t_{\mathrm{now}}$(当前时刻)查询发起的时间。
    tnowt_{\mathrm{now}}
  3. $R_{\mathrm{freshness}}$(新鲜度)两者之差,衡量湖中数据的最新程度。差值越小,说明从事件发生到可查询的延迟越低。
    RfreshnessR_{\mathrm{freshness}}

AI 应用场景

实时用户画像的增量更新

阶段:特征工程与特征存储

解决的问题:传统做法是把用户行为日志批量写入数仓,T+1 更新画像,特征滞后一天;而直接把流数据写入对象存储,又会出现并发覆盖导致画像错乱。事务层让数据湖支持 Upsert 与流式更新,行为事件以分钟级延迟合并进画像表,且每次合并都是一个原子事务。

具体案例:某推荐团队用 Hudi 存储用户实时特征,用户点击流经 Flink 清洗后每 5 分钟一次 Upsert 到湖中的画像表。查询引擎读取时通过 Merge-on-Read 合并增量文件,特征新鲜度从 24 小时降到 5 分钟,推荐点击率因此提升了 8%。

金融数据的审计与回滚

阶段:数据治理与模型合规

解决的问题:金融风控模型的训练数据需要可追溯、可回滚。如果上游修正了某天的数据,模型团队必须知道修正前后分别是什么样。传统数据仓库做不到低成本保留全量历史,对象存储的无版本覆盖又无法审计。时间旅行能力让分析师可以查询任意快照,并对误操作一键回滚。

具体案例:某支付公司的风控团队使用 Iceberg 存储交易事件表。一次上游脚本误更新了字段,分析师通过 TIMESTAMP AS OF 语法定位到出错前的快照,将表回滚到该版本,并用两份快照的差异定位了问题脚本,审计报告完整记录了全部操作链。

多团队共享的数据湖

阶段:数据中台与多团队协作

解决的问题:算法团队、报表团队、数仓团队共用同一个数据湖,各自向同一张表写入特征、清洗结果和汇总指标。没有事务控制时,后写的团队会覆盖前者的成果。乐观并发控制让各团队基于各自快照写入,提交时自动检测冲突,失败方基于最新版本重试,互不阻塞。

具体案例:某零售企业的湖上宽表由三个团队共同维护。采用 Delta Lake 事务日志后,并发写入冲突次数从每周十几次降到接近零,失败任务通过自动重试完成,数据团队之间的「互相踩踏」投诉消失了。

Python 演示

python59 行
# -*- coding: utf-8 -*-"""极简快照存储:模拟数据湖 ACID 的 Copy-on-Write 核心语义."""  class SnapshotLibrary:    """带快照隔离的简易键值存储.     写入不覆盖旧数据, 而是生成新版本, 读者可基于任意快照读取.    对应数据湖事务层: 多版本数据文件 + 清单指针切换.    """     def __init__(self):        self.versions = {}      # version -> state dict        self.current = 0        self.versions[0] = {}     def snapshot(self):        """返回当前版本号, 相当于读者领取快照卡."""        return self.current     def write(self, key, value):        """Copy-on-Write 写入: 复制当前状态, 修改副本, 提交新版本."""        new_state = dict(self.versions[self.current])  # 复制旧文件        new_state[key] = value                          # 在副本上修改        self.current += 1                               # 新版本号        self.versions[self.current] = new_state         # 提交快照        return self.current     def read(self, key, version=None):        """读取指定版本的数据, 默认为最新版本."""        v = self.current if version is None else version        if v not in self.versions:            raise KeyError("snapshot version not found")        return self.versions[v].get(key)     def time_travel(self, version):        """时间旅行: 返回某个历史版本的完整视图."""        return dict(self.versions.get(version, {}))  if __name__ == "__main__":    lib = SnapshotLibrary()    lib.write("user_001", {"name": "Alice", "age": 30})    v1 = lib.snapshot()     lib.write("user_001", {"name": "Alice", "age": 31})    v2 = lib.snapshot()     print("v1 快照看到的数据:", lib.read("user_001", v1))    print("v2 快照看到的数据:", lib.read("user_001", v2))    print("历史全量视图 v1:", lib.time_travel(1))     # 模拟乐观并发: 两个写者基于同一版本    base = v1    commit = lib.current    if commit == base:        print("乐观并发校验通过: 允许提交")    else:        print("乐观并发冲突: 需基于最新版本 v%d 重试" % commit)
目的:用不到 50 行实现一个带快照隔离、Copy-on-Write 和时间旅行的键值存储,直观演示数据湖事务层的三个核心机制。
  1. new_state = dict(self.versions[self.current])这行执行 Copy-on-Write 的「复制旧文件」:完整拷贝当前版本的数据文件,准备在副本上修改。
  2. self.current += 1每笔写入分配一个新版本号,等价于事务提交后元数据层切换版本指针。
  3. self.versions[self.current] = new_state把副本作为新快照挂到版本号上,旧版本原样保留,快照隔离由此成立。
  4. def time_travel(self, version)直接返回任意历史版本的状态字典,这就是数据湖时间旅行能力的微型版。
  5. lib.read("user_001", v1)读取时必须显式传入版本号,演示「读者固定在某个快照视角」的语义。
  6. if commit == base:乐观并发控制的核心判定:提交版本与基础版本一致才允许落盘,否则自动重试。

常见误区

误区我以为数据湖装上 Iceberg/Delta 之后,就跟 MySQL 一样支持行级更新、行级锁和复杂事务了,可以完全替代数据库。

正解数据湖事务层的事务粒度是「文件 + 快照」层面,不是数据库的行级事务。它保证的是「一批文件原子替换」「读者看固定快照」「写者提交不互相覆盖」,而不是让对象存储变成关系型数据库。你可以把事务层理解为给书库定了「整本换书」的规矩,而不是给每行文字上了锁。需要高频行级更新时,仍应使用数据库或 KV 引擎。

为何常见:因为 ACID 这个词太熟悉了,人们自然把数据库的所有能力都投射到数据湖上,忽略了事务粒度和使用场景的差异。

误区COW 意味着每次修改都要把整个数据文件复制一份,存储成本翻倍增长,数据湖很快就会爆掉。

正解COW 只复制「被修改的那个文件」,未涉及的文件在新旧快照之间是共享的——前文的动画里看到,v2 快照并没有复制 B,而是继续引用 v1 的 B。共享未改文件是对象存储的天然优势,因为文件不可变且按引用来组织。实际写入放大系数取决于文件大小和更新频率,系统还会通过「小文件合并」来抑制膨胀。

为何常见:只看「复制」二字,忽略了共享引用机制,把逻辑多版本的存储开销当成了物理全量复制。

误区既然快照都在,那我可以让数据湖保留所有历史版本,随时回到任意时刻,永不清除。

正解每个快照都引用一批文件,旧快照越多,占用的存储越大。生产环境通常会配置快照保留周期(如保留最近 7 天或最近 100 个快照),过期快照由后台清理任务删除。「时间旅行」只对未被清理的快照有效。选择保留周期是存储成本与可回溯深度的权衡。

为何常见:演示环境里数据量小,看不出成本;生产环境 TB 级数据下,无限保留会让存储账单迅速失控。

误区乐观并发控制反正会检测冲突并自动重试,那让写任务无限循环重试没关系,反正最终会成功。

正解重试确实能解决偶发冲突,但如果同一个数据点被高频并发写入,冲突会持续发生,任务可能陷入长时间的无效重试,甚至导致活锁——永远有人冲突、永远有人失败。此时应改用 Merge-on-Read 减少单次写入覆盖范围,或对热点数据做分桶、增加预处理合并,而不是让系统盲目重试。

为何常见:直觉上「失败-重试」是万能的,忽略了高冲突场景下重试成本呈指数上升的现实。

误区Iceberg、Delta、Hudi 都是实现 ACID 的组件,那我可以在同一张表上随意切换,甚至混合使用。

正解这三者是相互独立的表格式实现,各自定义了自己的元数据组织、事务日志格式和 API 规范。一张表只能由一种系统管理,切换意味着数据重写和迁移。选型时应结合计算引擎生态(如 Delta 对 Spark 友好)、写入模式(Hudi 对增量友好)、以及开放性需求(Iceberg 最中立)来决定,而不是当成可插拔的插件随意更换。

为何常见:初学者看到它们功能相似,误以为像数据库驱动一样可以随时替换,忽略了底层文件组织方式的根本差异。

面试题

Q1. 数据湖最初在对象存储上最突出的数据问题是什么?

单选题 · easy

  • A.无法保存海量非结构化数据
  • B.并发读写缺少事务保护,会产生覆盖和不一致← 正确答案
  • C.对象存储的容量太小
  • D.不支持列式存储格式

答案:B。对象存储只支持整体写入和覆盖,没有并发控制,多个作业同时读写时后写者会覆盖先写者,读者可能读到半成品。这正是事务层要补的短板。A、C 与事实相反,D 说的是文件格式问题而非一致性问题。

Q2. 快照隔离给读者带来的核心保证是什么?

单选题 · easy

  • A.每次查询都能看到最新提交的数据
  • B.在读取期间看到固定版本的数据视图← 正确答案
  • C.读者之间可以互相看到对方的未提交修改
  • D.写者可以强制刷新读者的缓冲区

答案:B。快照隔离让读者在事务开始时获得一个版本指针,整个读取过程都基于该指针解析文件,不受后续提交影响。A 属于读已提交的语义,C 是脏读,D 在数据湖场景中并不存在。

Q3. 在 Copy-on-Write 写入路径中,被修改的旧文件会怎样处理?

单选题 · mid

  • A.立即物理删除以释放空间
  • B.原样保留,继续被旧快照引用← 正确答案
  • C.原地覆盖为新内容
  • D.合并到另一个旧文件

答案:B。COW 的核心是「改副本不动原件」。旧文件仍然被旧快照的清单引用,因此必须原样保留,这也正是时间旅行能力的基础。A、C 会破坏快照一致性,D 是合并策略而不是 COW 本身的处理方式。

Q4. 以下哪些属于 ACID 事务的四个性质?

多选题 · mid

  • A.原子性(Atomicity)← 正确答案
  • B.一致性(Consistency)← 正确答案
  • C.分布式(Distributed)
  • D.持久性(Durability)← 正确答案

答案:ABD。ACID 指原子性、一致性、隔离性(Isolation)和持久性。C 的「分布式」是系统架构属性,不属于事务性质。本题考察对 ACID 定义的准确记忆,容易把相关但不同维度的概念混入。

Q5. 乐观并发控制提交时发现 V_commit ≠ V_base,正确的处理方式是?

单选题 · extreme

  • A.强制覆盖并提交,数据以本事务为准
  • B.丢弃写入并一直等待其他事务释放锁
  • C.放弃本次提交,基于最新版本重新计算后再次尝试提交← 正确答案
  • D.将目标数据临时改为只读

答案:C。乐观并发控制不加锁,冲突时让失败方基于最新版本重试是标准做法。A 会造成丢失更新,B 中乐观控制根本没有锁可等,D 改变了数据权限语义。实际系统中还会设置重试上限以避免活锁。

Q6. 数据湖的时间旅行能力可以被用于哪些场景?

多选题 · mid

  • A.查询历史上某个时刻的数据视图← 正确答案
  • B.把误操作的表回滚到事故前版本← 正确答案
  • C.提高高频写入的吞吐量
  • D.审计数据表随时间的演化过程← 正确答案

答案:ABD。时间旅行本质是「按版本读取快照」,因此历史查询、回滚、审计都依赖它。C 是写入模型的优化目标,与时间旅行无关,属于干扰项。

Q7. 相比 Copy-on-Write,Merge-on-Read 模式的主要代价是什么?

单选题 · mid

  • A.写入速度显著变慢
  • B.读取时需要合并基础文件和增量文件,查询变慢← 正确答案
  • C.完全不支持快照隔离
  • D.无法保存列式文件

答案:B。Merge-on-Read 把合并工作推迟到读取阶段,增量文件直接落盘,写得快但读得慢。它依然支持快照隔离(通过文件清单实现),也能保存列式格式,因此 C、D 错误,A 说反了。

Q8. 写放大系数 WA=(R+N)/N 中,R 代表什么?

单选题 · hard

  • A.读取查询的文件个数
  • B.因复制旧文件而额外写入存储的字节数← 正确答案
  • C.保留的历史快照数量
  • D.每笔事务的日志条数

答案:B。R 是重写旧文件的字节量,N 是新增有效数据字节量。WA 越接近 1 说明复制开销越小。A 是读路径指标,C 与版本保留有关,D 是元数据指标,均不参与写放大计算。

Q9. 以下哪些场景不适合使用乐观并发控制?

多选题 · hard

  • A.读多写少的报表查询场景
  • B.同一数据点被极端高频并发写入← 正确答案
  • C.必须严格保证写入串行顺序的核心账本系统← 正确答案
  • D.每批写入都基于最新快照的批处理任务

答案:BC。乐观并发控制假设冲突很少,适合读多写少(A)。B 中高频冲突会导致大量重试和活锁;C 需要确定性的写入顺序,乐观重试无法满足;D 总是基于最新快照计算,冲突概率低,适合乐观策略。

Q10. Delta Lake 通过什么机制记录每笔事务的变更?

单选题 · easy

  • A.每次提交时全量重写元数据 JSON
  • B.追加式事务日志(Transaction Log)← 正确答案
  • C.依赖外部分布式锁服务
  • D.直接修改表的 schema 文件

答案:B。Delta Lake 的核心是追加式事务日志,每次提交往日志里追加一条记录,所有表状态从日志重建。A 不是它的做法,C 是传统数据库的加锁思路,D 只是 schema 变更的一部分,不是事务记录机制。