上篇聊了整体架构与技术选型,这篇进入落地层面——怎么拆服务、拆多细、服务之间怎么通信。趣玩搭从 yudao-cloud 单体框架起步,按照 DDD 领域驱动思路,拆分为 15 个微服务,本文记录拆分过程中的思考与踩坑。


一、拆分原则

微服务不是拆得越细越好。30 人的团队维护 50 个服务,光是联调和运维就能把人累死。我们遵循三条底线:

  1. 一个服务对应一个限界上下文(Bounded Context),不跨领域聚合

  2. 一个服务由一个小组(2–4 人)完整负责,包括开发、测试和值班

  3. 能不拆就不拆——如果两个模块 90% 的变更总是一起发生,那它们就是同一个服务

二、服务全景图

基于以上原则,趣玩搭拆分为 15 个微服务,每个服务独立数据库、独立部署、独立伸缩:

服务名

领域

核心职责

关键依赖

funplay-gateway

网关

路由、鉴权、限流、灰度

Sentinel、Redis

funplay-user

用户

注册登录、用户画像、RBAC 权限

MySQL、Redis

funplay-activity

活动

活动 CRUD、定时上架、LBS 检索

MySQL、ES、Redis

funplay-ticket

票务

多规格票务、库存管理、候补队列

MySQL、Redis、MQ

funplay-order

订单

下单、退单、订单查询、幂等

MySQL(分库分表)、MQ

funplay-payment

支付

微信支付、钱包、退款、对账

MySQL、微信 API、MQ

funplay-member

会员

等级积分、权益、核销

MySQL、Redis

funplay-club

俱乐部

主理人认证、财务、对账

MySQL

funplay-social

社交

交友匹配、标签搜索、距离排序

ES、Redis GEO、Milvus

funplay-im

即时通讯

单聊群聊、消息推送

融云 SDK

funplay-content

内容审核

文本审核、图片审核

敏感词库、数美 API、LLM

funplay-marketing

营销

拉新、抽奖、积分商城

MySQL、Redis、MQ

funplay-ai

AI

知识库、智能客服

Milvus、LLM API

funplay-notify

消息中心

站内信、短信、微信模板、邮件

MQ、三方 API

funplay-job

任务调度

定时任务统一管理

XXL-JOB

三、几个关键拆分决策的推演

3.1 为什么票务和订单分开?

初版设计中,票务和订单放在同一个服务里。但实际跑下来发现两者的变更节奏和性能诉求差异很大:

  • 票务侧重于库存管理,核心操作是 Redis Lua 扣减,对延迟极度敏感(秒杀场景),变更频率低但稳定性要求极高

  • 订单侧重于状态机流转和持久化查询,是写入最密集的服务,需要独立做分库分表

拆开之后,票务服务可以独立扩缩容应对秒杀洪峰,订单服务可以独立做分片优化,互不影响。

3.2 为什么支付独立成服务?

支付涉及资金操作,安全等级最高。独立之后有三个好处:

  • 最小权限原则:只有 funplay-payment 持有微信商户密钥和证书,其他服务通过内部 RPC 调用支付能力,减少密钥扩散风险

  • 独立审计:支付服务的日志、变更、发布都可以单独走更严格的审批流程

  • 独立容灾:支付服务挂了,活动浏览和搜索不受影响,用户体验降级但不至于全站不可用

3.3 消息中心为什么不嵌入各业务服务?

站内信、短信、微信模板消息、邮件——这四个通道如果散落在各业务服务里,会出现大量重复代码和通道管理混乱。统一收口到 funplay-notify 之后:

  • 业务服务只需发送一条 MQ 消息(携带模板 ID + 参数 + 接收人),不关心具体走哪个通道

  • 消息中心统一管理通道优先级、频率限制、失败重试和用户免打扰配置

  • 新增通道(比如接入企业微信)只需在消息中心扩展,业务服务无感知

四、服务间通信规范

通信方式的选择直接决定了系统的耦合程度和可靠性。我们制定了一套简单但有效的规范。

4.1 同步调用:Dubbo Triple

适用场景:需要立即获取结果的调用,如下单时查询票务库存、支付时校验订单状态。

规范约束:

  • 超时统一 3 秒,重试 1 次

  • 熔断阈值:50% 错误率触发熔断,10 秒后半开探测

  • 所有 RPC 接口必须定义在独立的 api 模块中(Maven module),供调用方依赖

  • 禁止 A → B → C → D 超过三级的同步调用链,超过则改为异步

4.2 异步解耦:RocketMQ

适用场景:跨服务的写操作、非实时通知、最终一致性场景。

核心 Topic 定义:

Topic

生产者

消费者

说明

TICKET_SOLD

funplay-ticket

funplay-order

库存扣减成功,触发创建订单

ORDER_CREATED

funplay-order

funplay-notify

订单创建完成,发送通知

PAYMENT_SUCCESS

funplay-payment

funplay-order、funplay-member

支付成功,更新订单状态 + 积分

REFUND_APPLY

funplay-order

funplay-payment

申请退款

TICKET_REFUNDED

funplay-payment

funplay-ticket

退款完成,释放库存 + 触发候补

ACTIVITY_AUDIT

funplay-activity

funplay-content

活动发布,触发内容审核

规范约束:

  • 消息体统一使用 JSON,携带 msgId(UUID)用于幂等

  • 消费端必须实现幂等(MySQL 唯一索引 / Redis SETNX)

  • 消费失败走 RocketMQ 自带重试(最多 16 次指数退避),最终失败进入死信队列,告警人工处理

  • 所有跨服务写操作优先采用异步消息 + 最终一致,仅支付核心链路使用 TCC 分布式事务

4.3 一条典型链路串联

以"用户抢票下单支付"为例,看同步和异步如何配合:

用户点击抢票
    │
    ▼
[funplay-gateway] 限流 + 鉴权
    │ (HTTP)
    ▼
[funplay-ticket] Redis Lua 扣减库存
    │ (MQ: TICKET_SOLD)
    ▼
[funplay-order] 创建待支付订单
    │ (返回订单号给前端)
    │ (MQ: ORDER_CREATED → funplay-notify 发通知)
    ▼
用户确认支付
    │ (HTTP)
    ▼
[funplay-payment] 调起微信支付
    │ (微信回调)
    │ (MQ: PAYMENT_SUCCESS)
    ▼
[funplay-order] 更新订单状态为已支付
[funplay-member] 发放积分
[funplay-notify] 发送出票通知

这条链路中,只有「扣减库存」这一步是同步的(Redis Lua 操作本身在毫秒级),其余全部走 MQ 异步,最大限度降低了链路耦合和响应延迟。

五、数据库隔离策略

每个服务独占自己的数据库 Schema(逻辑隔离),禁止跨库 JOIN。需要聚合数据时有两种方式:

  1. RPC 查询:适用于低频、小数据量场景(如支付服务查订单状态)

  2. 数据冗余 / ES 宽表:适用于高频查询(如管理后台需要看「某活动下所有订单及支付状态」,通过 Canal 监听 Binlog 同步至 ES 宽表,毫秒级延迟)

分库分表策略(funplay-order):

  • user_id 取模分 8 库 16 表(ShardingSphere-JDBC)

  • 同一用户的订单落在同一分片,满足「我的订单」高频查询

  • 活动维度的聚合查询走 ES 宽表

六、踩坑总结

  1. 不要一开始就拆太细。我们最初把「充值」和「支付」拆成两个服务,后来发现它们共享同一套账务模型,合并后反而更清晰

  2. RPC 接口的 api 模块版本管理很重要。一定要语义化版本号,避免上游改了接口下游编译不过

  3. MQ 的 Topic 命名要规范。我们统一用 {动作}_{过去分词} 格式(如 TICKET_SOLD、ORDER_CREATED),语义清晰且不易冲突

  4. 每个服务第一天就要接入监控。不要等出了问题再加,SkyWalking 的 agent 挂载成本极低


下一篇趣玩搭关键技术方案:抢票秒杀、门票候补与钱包对账——深入三个最具技术挑战的场景,逐步拆解方案设计。


本站由 困困鱼 使用 Stellar 创建。