最后一章是 Kleppmann 的综合与观点:鉴于前十一章的所有工具和取舍,我们应该如何构建数据系统?他的答案围绕一个组织性想法——数据流(dataflow):把一个不可变事件的有序日志当作真相来源,把其他一切(数据库、索引、缓存、ML 特征)构建为从那个日志持续、异步计算的派生数据(derived data)。本章以一个对技术书不寻常的方式结束——对伦理的严肃讨论——因为数据系统如今塑造人们的生活。

⚡ 速览要点
  • 没有单一工具做一切——真实系统集成几个,所以你需要一种有原则的方式保持它们同步:全都从一个有序事件日志派生。
  • 解绑数据库(unbundling)——不用一个单体,而是组合专门的存储/索引/流系统,用日志作集成点("把数据库翻里朝外")。
  • 派生状态是物化视图——索引和缓存只是日志的预计算视图,由响应事件的流处理器维护。
  • 正确性需要端到端思考——exactly-once 和去重常无法在一层解决;你需要一个端到端标识符(如客户端生成的请求 ID)。
  • 完整性 > 及时性——许多应用容忍临时不一致(滞后)但绝不容忍数据丢失/损坏;优先完整性并验证它(审计,别盲信)。
  • 伦理是工作的一部分——预测分析、偏见、监控和同意是工程关切,不是别人的问题。
tldr

Kleppmann 主张的未来:停止试图找一个做一切的数据库,而是把一个不可变事件日志当作记录系统,同时围绕它组合专门的派生系统(解绑数据库)。用数据流构建派生数据——流处理器维护物化视图。端到端追求正确性(由请求 ID 标识的幂等操作、完整性优于及时性、审计)。并为你构建之物的人类影响负责。

数据集成(Data Integration)

因为没有单一工具满足所有访问模式,每个非平凡应用最终组合几个——一个事务数据库、一个搜索索引、一个缓存、一个分析仓库,也许一个推荐系统。核心实际问题是随数据变化保持它们彼此一致。本书反复的答案:挑一个记录系统(system of record)(权威来源)并把其他一切当作从它计算的派生数据,通过把记录系统的变更日志(经 CDC 或事件溯源)按定义的顺序喂进每个派生系统来保持同步。一个清晰的写全序是让这可靠的东西;没有它,对不同系统的并发更新竞争并分歧。

批与流,一起

批处理和流处理都是派生数据的工具;它们主要区别在输入是否有界。Lambda 架构提议同时跑两者——一个批层从所有历史重算准确视图、一个速度层给低延迟近似结果——但维护两条代码路径很痛苦。Kleppmann 主张趋势是统一它们:一个强大的流处理器也能重处理历史数据(从头重放日志),所以你能在改代码时从头重建一个派生数据集,而无需单独的批系统。

解绑数据库

传统数据库捆绑许多特性:一个存储引擎、二级索引、复制、查询优化器、物化视图。Kleppmann 把整个应用重新设想为一个翻里朝外的数据库:不用一个内部做一切的单体,你把这些特性解绑成独立、专门的系统,并用一个事件日志作集成点把它们接在一起。索引、缓存和物化视图成为独立系统,各订阅日志并异步维护自己的派生状态。

数据流:作为派生函数的应用代码

在这个视角里,应用代码成为一组响应事件并产生派生输出的派生函数(derivation function)——很像电子表格,改一个单元格自动重算下游一切。流处理器维护派生状态;当一个新事件到达,相关视图被更新。这把工作从读路径移到写路径:一个物化视图在数据变化时(写时)做一次昂贵计算,所以读成为便宜查找。取舍是经典的——更多写时工作和存储以换更快、更简单的读。

方面读路径(读时计算)写路径(物化视图)
工作何时发生查询时数据变化时
读延迟较高(每次重算)较低(预计算查找)
写成本 / 存储较高(维护视图)
最适合读罕见、数据巨大读频繁、需低延迟

追求正确性

若派生数据被异步维护且只是最终一致,我们如何保持应用正确?Kleppmann 对依赖任何单一机制(甚至事务)持怀疑,并推动一种更健壮的心态。

端到端论证(End-to-End Argument)

许多正确性保证只能端到端实现,而非由任何单层。经典例子是去重:TCP 重传、流处理器重试、事务可能被重新提交——所以同一操作能不止一次到达数据库。若用户点了两次,没有更低层能判断两个支付请求是"同一个"。修法是一个端到端幂等键(idempotency key):客户端生成一个唯一请求 ID,操作针对它做成幂等,这样栈里任何地方的重复都被吸收。

end-to-end idempotence with a request ID
# 客户端生成 ID 一次,随每次重试发送
POST /payments   request_id = "a1b2-c3d4"   amount = 50

server:
  if seen(request_id):        # 来自任何地方重试的重复
      return stored_result     # 被吸收——无双重扣款
  else:
      result = charge(50)
      record(request_id, result)

  来自 TCP、应用或流处理器的重试全都坍缩
  到一个效果——正确性端到端强制,而非每层。

及时性 vs 完整性

Kleppmann 分开人们混为"一致性"的两件事。及时性(timeliness)意味用户看到系统处于最新状态(无陈旧读);临时违反只是滞后,通常自我纠正。完整性(integrity)意味没有损坏——无丢失或矛盾的数据、无凭空创造或销毁的钱;违反是永久且严重的。实践中,完整性远比及时性要紧。许多应用乐于容忍几秒不一致(搜索索引落后数据库),只要完整性从不被违反。基于日志的派生数据系统有吸引力正因为它们使完整性容易:从一个不可变、有序日志的确定派生即便滞后也不能丢失或损坏数据。

信任,但要验证

最后,别假设你的系统正确——软件有 bug、磁盘悄悄损坏数据,且"它还没坏"不是保证。Kleppmann 倡导构建持续审计(audit)自己的系统:检查不变量、对照源验证派生数据,并使检测(并从中恢复)损坏成为可能,而非几年后才发现它。一个不可变事件日志在这里也有帮助——你总能重新派生并比较。

做正确的事

本书以伦理收尾,坚持工程师不能把数据系统的人类后果当成别人的问题。关切:

行动号召:把对用户的尊重——他们的尊严、隐私和自主——当作一等工程要求,而非事后想法。

总结

DDIA 结束于它开始的地方——可靠性、可扩展性、可维护性——但围绕数据流重构:一个不可变日志作真相来源、从它计算的派生数据、端到端追求的正确性、完整性优先于及时性。它加上一个最后的、非技术的支柱:工程师对这些系统对人做了什么的责任。构建正确、可审计、人道的系统。

🎯 面试速答

"解绑数据库"是什么意思?围绕一个事件日志作集成点组合专门系统(存储、索引、缓存、流处理器),而非一个单体数据库——"把数据库翻里朝外"。
为什么正确性是端到端关切?重试在许多层发生,所以去重需要一个端到端标识符(客户端请求 ID)和幂等操作——没有单层能保证它。
及时性 vs 完整性?及时性 = 最新(一个滞后,自我纠正);完整性 = 无损坏/丢失(永久、严重)。完整性更要紧,基于日志的派生使它容易。
读路径 vs 写路径?物化视图把工作移到写时,使读成为便宜查找——用写成本和存储换读延迟。

← 上一篇
流处理