喜欢把枯燥的技术文档变成"手把手教程",不讲空话,只讲怎么连、怎么写、怎么优化。
微服务拆分,最难的往往不是服务本身,是数据库。
代码拆成十个服务很简单,把 controller 移过去、改个包名、跑通就行。但数据库怎么拆?每个服务一个独立库还是共享?跨库 JOIN 怎么做?一个服务改了表结构,其他五个服务跟着报错,这怎么解?
今天把微服务架构下的数据库治理问题讲透。从拆分策略到跨库一致性到版本管理,跟着我走一遍,你也能搭一套合理的微服务数据库架构。
一、共享库的代价——为什么不能"一个库所有人用"
先说说最常见的起步方式:微服务拆了,数据库不动。五个服务连同一个库,大家共用同一套表。
刚起步的时候没问题。三个服务、几十个表、两个开发,共享一个库效率最高。
但规模上来之后,问题就来了:
问题一:schema 变更互相影响。服务 A 给 orders 表加了一个字段,服务 B 的 ORM 映射没更新,直接报错。更麻烦的是,服务 C 依赖那个字段做查询,但服务 A 还没发布,导致服务 C 上线后查不到数据。
问题二:连接数爆炸。每个服务都有自己的连接池,假设每个服务 20 个连接,10 个服务就是 200 个连接。数据库的 max_connections 是有限的,每个连接还占用内存。服务越多,连接压力越大。
问题三:性能干扰。服务 A 跑了一个大报表查询,把数据库 CPU 打满了。服务 B 的在线交易跟着变慢——明明两个服务完全无关,却因为共享数据库互相拖累。
问题四:权限无法隔离。服务 A 只需要读用户表,但因为连的是同一个库,它理论上可以访问所有表。权限粒度做不到服务级别。
关键判断点:如果你的服务数量超过 5 个、开发团队超过两个、schema 变更开始出现跨服务影响,共享库的模式就该结束了。
二、数据库拆分策略——按什么规则拆?
共享库不行,那就拆。但怎么拆?有三种主流策略。
策略 A:一个服务一个数据库
每个微服务拥有自己独立的数据库实例。服务之间不共享任何数据,只能通过 API 交互。
优点:隔离性最好。schema 变更互不影响、连接数可控、权限天然隔离。
缺点:跨服务查询困难。原来一个 JOIN 能搞定的事,现在得调 API。数据库实例多,运维成本上升。
适合:服务边界清晰、跨服务查询少的场景。
策略 B:按业务域拆分
不是每个服务一个库,而是按业务域分组。比如"订单域"包含订单服务、支付服务、物流服务,它们共享一个订单域数据库。"用户域"包含用户服务、权限服务、消息服务,共享一个用户域数据库。域与域之间不共享数据。
优点:平衡了隔离性和复杂度。域内服务可以共享数据库,跨域服务通过 API 或数据同步交互。
缺点:域边界需要仔细设计。分错了,域内服务还是会有 schema 冲突。
适合:服务数量多、但业务域清晰的场景。
策略 C:核心/边缘分离
核心业务(交易、支付、用户)用独立数据库,边缘业务(日志、统计、通知)共享一个数据库。核心严格隔离,边缘适度共享。
优点:资源集中在核心系统,边缘系统降低运维成本。
缺点:边缘服务之间仍有共享库的问题,只是范围缩小了。
适合:团队规模有限、但核心系统对稳定性和性能要求高的场景。
三、跨库数据一致性——不能 JOIN 了,怎么保证数据对得上?
拆了库之后,最大的挑战来了:原来在一个库里用事务 + JOIN 就能保证一致性的操作,现在跨库了,怎么做?
方案一:Saga 模式——用业务补偿代替技术回滚
Saga 的思路是:把一个跨服务的长事务拆成多个本地短事务,每个服务执行自己的事务,如果中间某一步失败了,就依次执行前面各步的"补偿操作"来撤销。
举例:电商下单流程涉及三个服务:订单服务(创建订单)、库存服务(扣减库存)、支付服务(扣款)。
- 第一步:订单服务创建订单,状态为"待支付"。
- 第二步:库存服务扣减库存。如果库存不足,调用订单服务的补偿接口,把订单状态改为"已取消"。
- 第三步:支付服务扣款。如果扣款失败,调用库存服务的补偿接口(恢复库存),再调用订单服务的补偿接口(取消订单)。
关键:每个服务必须实现正向操作和补偿操作。补偿操作不是"删除数据",是"把状态恢复到操作前"。
方案二:TCC 模式——Try-Confirm-Cancel
TCC 比 Saga 更精细,分三个阶段:
- Try:各服务预留资源(比如库存服务冻结库存,支付服务冻结额度),但不真正提交。
- Confirm:所有服务 Try 都成功后,统一 Confirm,真正提交。
- Cancel:任一服务 Try 失败,统一 Cancel,释放预留资源。
TCC vs Saga 的区别:TCC 在 Try 阶段就锁定了资源,保证了更强的隔离性;Saga 的正向操作直接提交,靠补偿回滚,隔离性弱但实现简单。
方案三:本地消息表——最朴素但最可靠
本地消息表的思路是:每个服务在执行本地事务的同时,往自己的"消息表"里插入一条记录。这条记录记录了"我做了什么,需要通知谁"。然后有一个独立的消费者扫描消息表,把消息推给目标服务。
举例:订单服务创建订单后,在本地事务中同时插入一条消息记录{target: "inventory", action: "deduct", orderId: "xxx"}。消费者扫描到这条消息,调用库存服务扣减库存。库存服务处理完后,回传确认。如果库存服务处理失败,消息状态保持"待处理",消费者会重试。
为什么可靠:消息的插入和业务操作在同一个本地事务中,要么都成功要么都失败,不会出现"业务做了但消息没发"的情况。
为什么不推荐用分布式事务框架直接上 2PC:两阶段提交在高并发场景下性能很差。持有锁的时间长、容易死锁、一个节点慢全体等。生产环境里,2PC 用得很少。
四、跨库查询怎么解决——不 JOIN 了,数据怎么拼?
拆了库之后,原来一条 SQL 就能搞定的跨表查询,现在数据分散在不同库里了。怎么办?
方法一:API 聚合
查询时,应用层分别调用各服务的 API,拿到数据后在内存中拼接。
优点:实现简单,不需要额外的数据同步。
缺点:多次网络调用,延迟高。不适合需要频繁跨库查询的场景。
适合:低频查询、数据量小的场景。
方法二:CDC 数据同步 + 宽表
用 CDC(Change Data Capture)工具实时捕获各服务的数据库变更,同步到一个集中的查询库(或宽表)中。查询时只读这个集中库,不需要跨库。
优点:查询性能好,一次查询搞定。
缺点:需要维护 CDC 管道,数据有延迟(通常秒级)。
适合:高频查询、报表类场景。
方法三:CQRS 读写分离
写操作走各服务的独立数据库,读操作走一个专门构建的查询库(读模型)。读模型通过事件驱动从各服务同步数据。
优点:读写彻底解耦,读模型可以按查询需求自由设计。
缺点:架构复杂度最高,需要完整的事件体系。
适合:读写比例悬殊、查询模式复杂的场景。
五、数据库版本管理——每个服务独立演进,怎么管?
拆了库之后,每个服务有自己的数据库 schema。版本管理怎么做?不能靠手动跑 SQL 脚本。
工具选择
Flyway:基于 SQL 脚本的版本管理工具。每个变更是一个独立的 SQL 文件,按版本号顺序执行。简单、可靠、学习成本低。
Liquibase:基于 XML/YAML/JSON 的变更日志管理。支持回滚脚本、差异生成。功能更强但学习成本高。
微服务环境下的实践
原则一:每个服务自带数据库版本管理。服务启动时自动检查并执行未应用的变更脚本。不要把版本管理放到 CI/CD 流水线里做,它应该是服务的一部分。
原则二:变更脚本只改自己服务的库。跨库变更通过数据同步或 API 协调,不要在一个服务的变更脚本里改其他服务的表。
原则三:向后兼容。改 schema 时,保证旧版本服务也能正常运行。比如加字段而不是改字段,旧代码不会用到新字段,所以不会报错。
# Flyway 脚本命名规范 V1__create_orders.sql -- 创建订单表 V2__add_order_status.sql -- 增加订单状态字段 V3__create_order_items.sql -- 创建订单项表注意:不要跳过版本号。Flyway 严格按版本号顺序执行,跳过会导致版本不一致。
六、公共数据的处理——用户、权限、字典表放哪?
有些数据是多个服务都需要用的:用户信息、权限数据、字典/配置表。放哪个库?
方案一:归口服务管理。用户数据归"用户服务"管,权限数据归"权限服务"管。其他服务需要这些数据时,通过 API 查询或 CDC 同步。
方案二:独立基础库。建一个"基础数据"库,存放用户、权限、字典等公共服务数据。所有服务只读不写这个库,写操作只能通过对应的管理服务。
方案三:各服务各自冗余。每个服务缓存自己需要的公共数据(比如订单服务缓存用户昵称)。通过消息机制保持缓存更新。适合读多写少的场景。
我的建议:用户和权限用方案一(归口服务管理),字典和配置用方案三(各服务缓存)。基础库方案增加了架构复杂度,除非你的公共数据量很大。
对比:五种微服务数据库模式
| 模式 | 隔离性 | 复杂度 | 跨库查询 | 适用阶段 |
|---|---|---|---|---|
| 共享库 | 低 | 低 | 一个 JOIN 搞定 | 起步期(< 5 服务) |
| 一服务一库 | 高 | 中 | API 聚合或 CDC | 成长期(5-20 服务) |
| 按域拆分 | 中 | 中 | 域内 JOIN,域间 CDC | 成熟期(20+ 服务) |
| 核心/边缘分离 | 核心高、边缘低 | 低-中 | 核心库独立,边缘可 JOIN | 团队规模有限 |
| CQRS 读写分离 | 高 | 高 | 读模型一次查询 | 读写比例悬殊 |
决策框架:按三个维度选择拆分策略
维度一:服务数量
- < 5 个:共享库即可,先跑起来再说
- 5-20 个:一服务一库或按域拆分
- 20+ 个:按域拆分 + 域内共享
维度二:团队规模
- 1-2 个开发团队:核心/边缘分离,降低运维负担
- 3-5 个开发团队:按域拆分,每个团队负责一个域
- 5+ 个团队:一服务一库 + CQRS
维度三:数据耦合度
- 低(服务间数据交互少):一服务一库
- 中(有跨服务查询但不频繁):按域拆分 + 域内共享
- 高(频繁跨服务查询):CDC 宽表 + CQRS 读模型
微服务数据库治理检查清单
拆分前评估:
- 梳理现有共享库的表,标记哪些表被哪些服务使用
- 识别跨服务的数据依赖关系(A 服务写、B 服务读的表)
- 评估拆分后的跨库查询需求,提前规划解决方案
拆分后验证:
- 确认每个服务的数据库连接独立,连接数在合理范围
- 确认 schema 变更不会影响到其他服务
- 确认跨库一致性方案(Saga/TCC/本地消息表)的补偿逻辑正确
- 确认公共数据的归口管理方案已落实
日常运维:
- 每个服务的数据库版本管理脚本已纳入代码仓库
- 跨库数据同步的延迟在监控范围内(CDC 延迟 < 5 秒)
- 定期演练补偿逻辑,确保异常场景下能正确回滚
- 新增服务时,按拆分策略分配数据库资源,不要走共享库的老路
总结
微服务数据库治理的核心就三条:
拆要拆到位——按服务或按业务域隔离,不要共享库走到黑。
一致性选对方案——不要一上来就搞 2PC,Saga 或本地消息表在大部分场景下够用。
版本管理自动化——每个服务自带数据库版本管理,变更脚本向后兼容。
实际项目里,我见过最惨的情况是 15 个服务共享一个库,改一个字段要拉五个团队开会。拆了之后,每个团队管自己的库,效率高了很多,跨库数据用 CDC 同步,延迟在秒级可接受。
这条路每个做微服务的团队都要走,早点规划比后期重构好。
后续我会继续分享分布式事务落地实战、CDC 数据同步方案对比这些话题,跟着我一篇篇学,数据库这块就没问题了。
有问题评论区见。
喜欢把枯燥的技术文档变成"手把手教程"。关注我,数据库这块我们一起搞定。