05 - 数据怎么办:每个服务一个数据库
这一章讲微服务里最反直觉、也最容易踩坑的一条原则:
每个服务,管自己的数据库。别人不许直接来查。
先看一个”看起来很方便”的错误做法
拆成微服务后,很多人第一反应是:服务归服务,但数据库还是共用一个吧,这样订单服务想要用户数据,直接连过去查一下多方便。
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│用户服务 │ │商品服务 │ │订单服务 │ │支付服务 │
└───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘
└───────────┴─────┬─────┴───────────┘
▼
┌────────────────────┐
│ 一个共享的大数据库 │ ← 谁都能直接读写所有表
└────────────────────┘
方便是方便,但这等于又把大家焊回去了——正是我们拆微服务想逃离的东西。
为什么共享数据库是个陷阱
陷阱 1:又变回”牵一发动全身”
订单服务嫌 users 表的某个字段不好用,想改表结构。可用户服务、支付服务也在读这张表……一改,全崩。数据库又变成了那个”谁都不敢动”的焊死点。
陷阱 2:谁也不知道数据被谁动了
四个服务都能写 orders 表,出了脏数据,根本查不出是哪个服务干的。责任边界一片模糊。
陷阱 3:没法各自扩容、各自选型
订单数据量暴涨想单独优化?想给商品用别的数据库?——共用一个库,动不了。
正确做法:每服务一库
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│用户服务 │ │商品服务 │ │订单服务 │ │支付服务 │
└───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘
▼ ▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│用户库 │ │商品库 │ │订单库 │ │支付库 │
└───────┘ └───────┘ └───────┘ └───────┘
规矩:想要别人的数据?不能直接连它的库,
必须走它的接口去"问"(回到第 04 章的通信方式)。
这样每个服务的数据是私有的,别人只能通过它对外的接口来拿。它内部想怎么改表、换什么数据库,都不影响别人。数据也解耦了。
但新问题来了:跨服务的一致性
这是微服务最头疼的问题。看”下单”这个动作,它其实要动两个服务的数据:
- 订单库:创建一条订单
- 商品库:把库存减 1
在单体里,这俩操作能放进一个数据库事务,要么全成功、要么全失败(你在 PostgreSQL 里学过的事务)。可现在它们在两个库里,一个事务包不住了。万一:
订单创建成功了 ✅,但扣库存请求失败了 ❌ —— 用户下了单,库存却没减,超卖了。
应对思路(只讲思想,不抠实现)
微服务里,我们放弃了”任何时刻数据都严丝合缝”的执念,换成一个更现实的目标:
最终一致性:允许中间有极短暂的不一致,但通过一些机制,保证最后数据一定会对上。
一个常见的落地思路,正好用上你学过的消息队列:
1. 订单服务:先把订单创建了(自己库里的事,稳)
2. 订单服务:往消息队列丢一条"订单已创建,请扣库存"
3. 商品服务:从队列取到消息,去扣库存
├─ 扣成功 → 完事
└─ 扣失败 → 消息重试,或发一条"扣减失败"事件
触发订单服务把这单取消(补偿)
核心思想两句话:
- 用异步消息把多个服务的动作串起来,别指望一个大事务锁住全部;
- 万一中途失败,就用”补偿动作”把已经做的那步撤销(比如把订单标记为取消)。
这类”用一连串带补偿的步骤,来完成一个跨服务操作”的模式,业界叫 Saga。名字知道即可,思想才是关键——接受短暂不一致,靠消息和补偿达成最终一致。
小结
- 微服务铁律:每个服务管自己的库,别人只能通过接口来要数据,不能直连别人的库。
- 共享数据库看着方便,实则把服务又焊死,是个陷阱。
- 代价是跨服务一致性变难:多个库没法用一个事务包住。
- 应对思想:追求最终一致性,用消息队列串联 + 失败时补偿,而不是强求时时刻刻严丝合缝。
上一章 ← 04 - 服务之间怎么”说话” | 下一章 → 06 - 一套微服务系统的全景架构 | 回到 README 目录