服务拆分方法论:按业务能力拆分、按子域拆分、拆分粒度控制、拆分后的数据一致性考量
好,咱们进入第三章。这一章我打算聊聊服务拆分的方法论。
说实话,很多团队一上来就撸起袖子拆服务,结果拆完发现比没拆还痛苦。为什么?因为拆分不是拍脑袋,得有章法。我个人习惯把拆分方法论归纳为四个核心问题:按什么拆?拆多细?拆完数据怎么办?
一、按业务能力拆分:最直观的切分方式
这是最基础、也最容易上手的方法。说白了,就是把系统里每个独立的业务能力拎出来,变成一个服务。
举个例子,一个电商系统,你想想看,有哪些核心能力?
- 用户管理:注册、登录、权限
- 商品管理:商品发布、库存、分类
- 订单管理:下单、支付、退款
- 物流管理:配送、轨迹、签收
每个能力都可以独立成一个微服务。我在项目中遇到过这样一个场景:早期我们把用户和订单揉在一起,结果每次用户中心升级,订单就得跟着停服。拆开之后,两边各自迭代,互不干扰。
核心原则:一个服务只做一件事,并且把它做好。
但这里有个坑——业务能力的边界怎么界定? 我见过有人把「发送短信验证码」也拆成一个服务,这就过度了。判断标准很简单:这个能力是否经常独立变化?是否有独立的资源需求?如果答案都是否,就别拆。
二、按子域拆分:DDD 的落地实践
如果你觉得按业务能力拆太粗糙,那可以试试领域驱动设计(DDD)里的子域拆分法。这个方法更精细,也更考验对业务的理解。
DDD 把业务领域分成三类:
| 子域类型 | 特点 | 示例 |
|---|---|---|
| 核心域 | 公司的核心竞争力,投入最多资源 | 电商的推荐算法、支付引擎 |
| 支撑域 | 业务必需,但非核心 | 用户管理、商品管理 |
| 通用域 | 通用能力,可复用 | 短信通知、日志收集 |
我建议的做法是:核心域优先拆,支撑域按需拆,通用域能复用就复用。我曾经在一个项目中,把核心的「风控引擎」拆成了独立服务,结果后来业务量暴涨,其他服务都扛不住了,只有风控服务稳如泰山。为什么?因为它的拆分粒度刚好匹配了业务复杂度。
小技巧:画一张「业务领域地图」,把每个子域的边界、依赖关系、数据流都标出来。这张图就是你的拆分蓝图。
三、拆分粒度控制:拆太细 vs 拆太粗
这是最让人头疼的问题。拆太细,服务满天飞,运维成本爆炸;拆太粗,又回到了单体架构的老路。
我个人总结了一个「三问法」来判断粒度是否合适:
- 这个服务是否独立部署? 如果每次改动都要牵动其他服务一起发布,说明拆得不够。
- 这个服务的数据是否独立? 如果两个服务频繁跨库 join,说明拆得太细了。
- 这个服务的团队是否独立? 如果一个人要维护 10 个服务,那粒度肯定有问题。
举个例子,我曾经接手过一个项目,他们把「用户头像上传」和「用户信息查询」拆成了两个服务。结果每次用户登录,前端要调两次接口。你说这合理吗?不合理。头像上传本质上是用户信息的一部分,合并成一个服务更合适。
避坑指南:我曾经见过一个团队,把 20 个服务拆成了 200 个,结果光服务注册中心就挂了三次。记住,微服务的核心是「微」,但不是「碎」。
这里我给出一个参考的粒度标准:
- 一个服务:对应一个业务子域,通常 3-5 个开发人员维护
- 一个服务:数据量在百万级以内,接口调用量在千级/秒以内
- 一个服务:部署后启动时间不超过 30 秒
四、拆分后的数据一致性考量:绕不开的难题
服务拆了,数据库也跟着拆了。以前在一个库里用事务就能搞定的事,现在得跨服务处理。数据一致性就成了大问题。
我常用的策略有三种:
1. 最终一致性 + 补偿机制
这是最常用的方案。比如下单扣库存,订单服务先创建订单,然后发消息给库存服务扣库存。如果库存不够,订单服务再回滚订单。
// 伪代码示例:下单流程
public void createOrder(Order order) {
// 1. 创建订单(本地事务)
orderRepository.save(order);
// 2. 发送扣库存消息(可靠消息)
messageSender.send("inventory.deduct", order);
// 3. 如果库存扣减失败,消费补偿消息
// 监听 "inventory.deduct.failed" 主题
// 执行 orderRepository.cancel(order);
}
2. 本地消息表 + 定时任务
这个方案我特别喜欢,因为它简单可靠。在订单库里建一张消息表,业务操作和消息写入在同一个本地事务里。然后定时任务扫描消息表,把消息发送出去。
// 本地消息表示例
CREATE TABLE local_message (
id BIGINT PRIMARY KEY,
business_id VARCHAR(64),
message_body TEXT,
status TINYINT DEFAULT 0, -- 0:待发送, 1:已发送, 2:已确认
create_time DATETIME
);
// 定时任务伪代码
@Scheduled(fixedDelay = 5000)
public void sendPendingMessages() {
List<LocalMessage> messages = messageDao.selectByStatus(0);
for (LocalMessage msg : messages) {
boolean success = messageSender.send(msg.getMessageBody());
if (success) {
messageDao.updateStatus(msg.getId(), 1);
}
}
}
3. TCC(Try-Confirm-Cancel)模式
这个模式适合对一致性要求极高的场景,比如支付、转账。但实现起来比较复杂,我一般只在核心链路里用。
我的建议:80% 的场景用「最终一致性 + 补偿机制」就够了。别一上来就上 TCC,那是给自己找麻烦。
五、总结一下
服务拆分没有银弹。按业务能力拆分适合快速上手,按子域拆分适合精细化治理。粒度控制上,记住「三问法」就能避免大部分坑。数据一致性方面,优先选最终一致性,别过度设计。
嗯,这一章就聊到这儿。下一章我会讲讲服务间的通信与治理,到时候咱们再细聊 RPC 和消息队列的那些事儿。