商城系统开发(八):微服务拆分与服务治理

随着商城系统功能越来越多,单体架构的问题开始暴露:部署慢、模块耦合严重、一个服务出问题整个系统受影响。这篇介绍将商城系统从单体拆分为微服务的完整方案,包括服务划分策略、通信方案选择、服务发现、配置中心、链路追踪和网关的搭建。

为什么拆

拆之前先说说不拆的好处:单体简单、部署快、调试方便、不存在分布式事务问题。很多中小项目根本不需要微服务。

但当系统规模增长到一定程度时,以下问题会逐渐显现:

  • 代码库膨胀:代码量增长导致编译变慢、IDE 响应变差
  • 多人协作冲突:多个开发者同时修改不同功能但同一模块的代码,合并冲突频繁
  • 部署粒度太粗:改了一行搜索逻辑,整个系统都要重新部署
  • 资源浪费:搜索服务需要高 CPU,订单服务需要高内存,但它们跑在同一个进程里,无法独立扩缩容
  • 故障隔离差:某个模块的异常可能拖垮整个系统

拆分的原则:按业务领域划分,高内聚低耦合

服务划分

根据领域驱动设计(DDD)的思路,商城系统可以拆成 6 个核心服务 + 3 个基础设施服务:

核心业务服务:

  1. 商品服务(product-service):商品的增删改查、分类管理、SKU/SPU 管理、库存预占
  2. 订单服务(order-service):下单、支付回调、订单状态机、退款
  3. 用户服务(user-service):注册登录、用户信息、收货地址、会员体系
  4. 支付服务(payment-service):支付渠道对接(微信/支付宝)、支付记录、退款处理
  5. 库存服务(inventory-service):库存增减、库存预占与释放、库存流水
  6. 搜索服务(search-service):商品搜索、筛选、排序、搜索建议

基础设施服务:

  1. API 网关(gateway):路由、鉴权、限流、熔断
  2. 配置中心(config):集中管理所有服务的配置
  3. 消息服务:异步事件分发(订单创建通知、库存扣减等)

划分时的考虑点:

  • 库存要不要独立出来? 库存通常是商品服务的一部分。但如果库存逻辑(预占、释放、超卖防护)复杂度较高,和商品的 CRUD 逻辑完全不同,独立出来更好维护。
  • 支付要不要独立出来? 支付涉及资金安全,需要最高级别的稳定性和安全审计,建议独立部署。
  • 搜索服务的数据从哪来? 商品服务通过消息队列把数据变更事件发给搜索服务,搜索服务维护自己的 Elasticsearch 索引。这是最终一致性,不是强一致。

服务通信:gRPC

服务之间的同步调用推荐 gRPC,理由:

  • 性能:Protobuf 二进制序列化,比 JSON 快 5-10 倍
  • 类型安全:proto 文件定义接口,编译时就能发现问题
  • 代码生成:proto 编译后自动生成客户端和服务端代码
  • 流式支持:支持双向流,适合实时推送场景

proto 文件定义示例(商品服务):

service ProductService {
  rpc GetProduct(GetProductRequest) returns (Product);
  rpc ListProducts(ListProductsRequest) returns (ListProductsResponse);
  rpc CheckStock(CheckStockRequest) returns (CheckStockResponse);
}

异步通信用消息队列(RabbitMQ)。下单流程就是典型的混合场景:

  1. 用户下单 → 订单服务创建订单(同步)
  2. 订单服务发消息 → 库存服务扣减库存(异步)
  3. 用户支付 → 支付服务回调订单服务(同步)
  4. 订单完成 → 发消息通知搜索服务更新销量(异步)

服务发现:Consul

选 Consul 而不是 Nacos 或 Etcd,主要因为:

  • 内置健康检查,不需要额外组件
  • 支持多数据中心
  • 提供 DNS 和 HTTP 两种服务发现接口
  • Go 语言写的,和 Go 技术栈一致

每个服务启动时向 Consul 注册,关闭时注销。Consul 通过心跳检测服务健康状态,不健康的实例会被自动摘除。

服务间调用时,先从 Consul 获取目标服务的实例列表,再通过负载均衡算法选择一个实例。可以使用加权轮询——给不同配置的机器设不同权重。

配置中心

配置管理是微服务的刚需。每个服务都有自己的数据库连接、Redis 地址、消息队列配置、业务参数。如果这些都写在各自的配置文件里,改一个数据库密码要改多个地方。

可以使用 Consul 的 KV 存储作为配置中心(不需要多引入一个组件)。配置按照 service-name/config-key 的路径组织:

product-service/db/host = "10.0.1.10"
product-service/db/port = "3306"
product-service/nedis/addr = "10.0.1.20:6379"
order-service/db/host = "10.0.1.11"
...

配置变更时,Consul 通过 Watch 机制通知服务热更新,不需要重启。

环境隔离通过 Consul 的 datacenter 或 namespace 实现:dev、staging、production 各一套配置,互不干扰。

链路追踪

微服务最头疼的问题之一就是排查问题。一个请求经过网关 → 订单服务 → 商品服务 → 库存服务,哪个环节慢了?哪个环节出错了?

链路追踪解决的就是这个问题。可以用 Jaeger 作为追踪后端,遵循 OpenTelemetry 标准。

核心概念:

  • Trace:一个完整请求的调用链,由一组 Span 组成
  • Span:一个操作单元(一次 RPC 调用、一次数据库查询)
  • TraceID:全局唯一标识,贯穿整个调用链

每个服务在处理请求时生成 Span,记录耗时、状态、关键参数,然后上报给 Jaeger。在 Jaeger UI 里可以看到完整的调用拓扑和时间线。

排查性能问题时特别有用。比如下单接口偶尔变慢,通过链路追踪可以快速定位到具体是哪个服务的数据库查询缺少索引。

网关

网关是微服务的统一入口,承担的职责:

路由:根据 URL 路径把请求转发到对应的服务。比如 /api/products/* 转发到商品服务,/api/orders/* 转发到订单服务。

鉴权:在网关层统一做 JWT 验证,下游服务不需要重复鉴权。验证通过后把用户信息(user_id, role)放到请求头里传给下游。

限流:基于令牌桶算法,按用户/IP/接口维度限流。秒杀场景下,网关层直接拦截超出阈值的请求,保护下游服务。

熔断:当下游服务故障率超过阈值,网关自动熔断,返回降级响应。避免故障雪崩。

日志与监控:记录所有请求的 method、path、status、latency,接入 Prometheus + Grafana 看板。

拆分后的架构

拆分完成后整体架构如下:

客户端 (App/Web)
    ↓
  API 网关 (认证/限流/路由)
    ↓
  ┌──────────────────────────────────────┐
  │  商品服务  订单服务  用户服务          │
  │  支付服务  库存服务  搜索服务          │
  └──────────────────────────────────────┘
    ↕ gRPC (同步)    ↕ RabbitMQ (异步)
  ┌──────────────────────────────────────┐
  │  Consul (服务发现+配置)               │
  │  Jaeger (链路追踪)                    │
  │  Prometheus + Grafana (监控)          │
  └──────────────────────────────────────┘
    ↓
  MySQL / Redis / Elasticsearch / RabbitMQ

每个服务有独立的数据库(不共享 schema),通过 API 交互。这是微服务的基本原则——数据自治

常见问题

分布式事务:下单涉及订单创建 + 库存扣减 + 支付,跨了三个服务。可以选择 Saga 模式——每个步骤都有对应的补偿操作,失败时逆序回滚。

数据一致性:搜索服务的商品数据通过消息队列同步,有延迟。用户刚修改了商品名称,搜索结果可能还是旧的。这个只能靠产品层面说明"搜索结果可能有几秒延迟"。

服务间循环依赖:订单服务调用商品服务查价格,商品服务又调用订单服务查销量。解决方案是商品服务不再实时查销量,而是通过消息队列异步更新销量缓存。

调试困难:单体时一个断点就能跟踪整个流程,微服务时需要同时启动多个服务。可以用 docker-compose 管理本地开发环境,加上链路追踪,提升调试效率。

后续

微服务拆完只是开始。后续还需要:

  • 容器化部署(Docker + Kubernetes)
  • CI/CD 流水线(每个服务独立构建部署)
  • 完善监控告警
  • 灰度发布