服务拆分方法论:按业务能力拆分、按子域拆分、拆分粒度控制、拆分后的数据一致性考量

好,咱们进入第三章。这一章我打算聊聊服务拆分的方法论。

说实话,很多团队一上来就撸起袖子拆服务,结果拆完发现比没拆还痛苦。为什么?因为拆分不是拍脑袋,得有章法。我个人习惯把拆分方法论归纳为四个核心问题:按什么拆?拆多细?拆完数据怎么办?

一、按业务能力拆分:最直观的切分方式

这是最基础、也最容易上手的方法。说白了,就是把系统里每个独立的业务能力拎出来,变成一个服务。

举个例子,一个电商系统,你想想看,有哪些核心能力?

  • 用户管理:注册、登录、权限
  • 商品管理:商品发布、库存、分类
  • 订单管理:下单、支付、退款
  • 物流管理:配送、轨迹、签收

每个能力都可以独立成一个微服务。我在项目中遇到过这样一个场景:早期我们把用户和订单揉在一起,结果每次用户中心升级,订单就得跟着停服。拆开之后,两边各自迭代,互不干扰。

核心原则:一个服务只做一件事,并且把它做好。

但这里有个坑——业务能力的边界怎么界定? 我见过有人把「发送短信验证码」也拆成一个服务,这就过度了。判断标准很简单:这个能力是否经常独立变化?是否有独立的资源需求?如果答案都是否,就别拆。

二、按子域拆分:DDD 的落地实践

如果你觉得按业务能力拆太粗糙,那可以试试领域驱动设计(DDD)里的子域拆分法。这个方法更精细,也更考验对业务的理解。

DDD 把业务领域分成三类:

子域类型 特点 示例
核心域 公司的核心竞争力,投入最多资源 电商的推荐算法、支付引擎
支撑域 业务必需,但非核心 用户管理、商品管理
通用域 通用能力,可复用 短信通知、日志收集

我建议的做法是:核心域优先拆,支撑域按需拆,通用域能复用就复用。我曾经在一个项目中,把核心的「风控引擎」拆成了独立服务,结果后来业务量暴涨,其他服务都扛不住了,只有风控服务稳如泰山。为什么?因为它的拆分粒度刚好匹配了业务复杂度。

小技巧:画一张「业务领域地图」,把每个子域的边界、依赖关系、数据流都标出来。这张图就是你的拆分蓝图。

三、拆分粒度控制:拆太细 vs 拆太粗

这是最让人头疼的问题。拆太细,服务满天飞,运维成本爆炸;拆太粗,又回到了单体架构的老路。

我个人总结了一个「三问法」来判断粒度是否合适:

  1. 这个服务是否独立部署? 如果每次改动都要牵动其他服务一起发布,说明拆得不够。
  2. 这个服务的数据是否独立? 如果两个服务频繁跨库 join,说明拆得太细了。
  3. 这个服务的团队是否独立? 如果一个人要维护 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 和消息队列的那些事儿。