DBAtlas · 数据库运维图谱

参考架构:微服务数据层

在微服务中,最关键的决策是数据所有权:每个服务必须拥有自己的数据(database-per-service),不得直接访问其他服务的表。这棵决策树逐步介绍共享数据库反模式、在不使用分布式事务的情况下实现跨服务一致性(saga + outbox)、用 CDC 同步数据,以及跨服务查询(API 组合 vs CQRS 读模型)。目标是让服务真正独立、可单独部署。

服务如何拥有数据:共享数据库还是每个服务一个?

数据隔离是微服务架构的支柱。服务是使用单个共享数据库(隐藏的依赖),每个服务拥有自己的数据库(正确的模式),还是真正的挑战在于跨服务一致性/查询?先确定当前状态。

  • 所有服务都使用单个共享数据库
  • 每个服务有自己的数据库,但操作需要跨服务一致性
  • 每个服务有自己的数据库,但需要组合查询/报表

如何保持跨服务工作流的一致性?

对于跨多个服务的操作(创建订单 → 收款 → 扣减库存),传统的分布式事务(2PC)既脆弱又无法扩展。应将流程拆分为本地事务 + 事件的步骤(saga),并可靠地发布事件(outbox)。你面对的是哪个问题?

  • 我正试图让多服务工作流保持原子性(2PC?)
  • 我无法让本地写入和事件发布保持一致(双写)

如何实现展示来自多个服务的数据的查询?

数据拆分到多个服务后,就无法用单个 join 构建组合页面。主要有两种方法:在请求时调用相关服务的 API 并合并结果(API 组合),或维护一个由事件驱动、预先连接好的读模型(CQRS)。查询的频率和复杂度决定了选择。

  • 简单、不频繁的连接;调用几个服务再合并即可
  • 复杂/频繁的组合查询;需要现成的读模型

建议:Database-per-service(拆分共享数据库)

多个服务共享一个数据库是一种隐蔽而危险的依赖:一个服务的模式变更会破坏其他服务,表的所有权变得不清晰,独立部署变得不可能。每个服务必须拥有自己的数据。

建议:Saga 模式(避免分布式事务)

不要试图用单个分布式事务(2PC)使跨多个服务的操作保持原子性;2PC 会持有锁,并带来单点脆弱性和低可用性。应将流程拆分为在各自服务中作为本地事务执行的步骤,失败时用补偿步骤撤销它们。

建议:事务性 outbox(双写陷阱)

当一个服务既要写入自己的数据库又要发布事件时,分开执行这两步就是“双写”陷阱:如果一个成功而另一个失败,系统就会处于不一致状态。解决方法是在与业务数据相同的本地事务中,将事件写入 outbox 表,然后由单独的进程发布。

建议:API 组合(在请求时合并)

在需要组合数据的地方,调用方(例如 gateway/BFF)调用相关服务的 API,并在内存中合并结果。对于跨少数服务的简单连接,这是最简单的方案;无需保留额外的数据副本。

建议:CQRS 读模型(由事件供给)

对于复杂且频繁的组合查询,维护一个由各服务发出的事件供给的、预先连接好并针对查询优化的读模型(物化视图/反规范化副本)。写入端保留在各服务中;读取端从一个地方快速提供现成的数据。

在 DBAtlas 中打开 →