参考架构:微服务数据层
在微服务中,最关键的决策是数据所有权:每个服务必须拥有自己的数据(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 读模型(由事件供给)
对于复杂且频繁的组合查询,维护一个由各服务发出的事件供给的、预先连接好并针对查询优化的读模型(物化视图/反规范化副本)。写入端保留在各服务中;读取端从一个地方快速提供现成的数据。