微服务的基本设计原则:别只会拆服务,要会「养服务」

34 阅读13分钟

微服务的基本设计原则:别只会拆服务,要会「养服务」

微服务不是把单体照着模块名一刀一刀切开就完事了。

很多团队上微服务,第一步就是「先拆一圈 Service」,结果:

  • 服务数量爆炸,调用关系成了意大利面
  • 任意一个小服务挂了,都能把主链路带崩
  • 测试困难、发布复杂、排查问题像走迷宫

这一章想帮你站在一线开发 / 准架构师的视角,搭起一套微服务设计的底层原则,包括:

  • 服务内如何分层、为什么要弱化传统 MVC 里的 Controller
  • 拆服务时,除了 DDD,还要看压力模型、主链路、用户群体
  • 无状态服务、接口版本控制、流量整形、限流与消息驱动这些「微服务标配」怎么落地
  • Base 理论、幂等性这些听起来抽象的概念,和你日常接口设计有什么关系

一、服务内部怎么分层:轻量分层,弱化 MVC Controller

很多项目至今还在习惯性使用传统 MVC:

  • Controller:处理请求 / 转发视图
  • Service:写业务逻辑
  • Dao:访问数据库

在早期服务端驱动页面渲染的年代(JSP、模板引擎),Controller 确实有价值:
负责把请求 dispatch 到不同视图。但在微服务 + 前后端分离时代:

  • 服务提供的是 API + JSON 数据,不再关心视图跳转
  • 前端(H5 / App)自己做路由与展示

于是,你的 Controller 往往只干两件小事:

  • 简单的数据封装 / 解封装
  • 把调用转发到 Service

这时候继续硬保留一层 heavy Controller 的意义就不大了,还多了一层样板代码。

推荐的分层方式(阿里系常见做法)

更轻量的分层通常是:

  • API 层(独立 Maven 模块)

    • 定义接口签名、DTO、VO
    • 作为 RPC / Feign / HSF 等调用的「二方包」对外发布
  • Service 层

    • 只关注业务逻辑,不关心视图、终端形态
    • 对外直接返回领域对象 / DTO(JSON),不必强制包一层统一返回体
  • DAO / Repository 层

    • 处理持久化逻辑(SQL / ORM / ES / NoSQL 等)

在 Service 下面,你可以再根据复杂度细拆:

  • Manager / DomainService 等,再结合设计模式做更优雅的拆分

核心思想是:

  • 把视图 / 展示的关注点从服务里剥离出去
  • 服务只负责「提供数据和行为」,前端自己决定怎么渲染

一个简单的实践建议:

  • 如果你的项目还是「Controller 层做一堆业务逻辑,Service 只是个薄壳」,可以逐步把逻辑往 Service 移,让 Controller 变轻,甚至在只对内的 RPC 服务里完全不用 Controller。

二、微服务拆分:别只记住「DDD」,还要看压力和主链路

拆服务,是微服务架构的第一步,也是最容易走偏的一步。

大家常听到的方案是:

  • 按领域模型(DDD)拆:商品域、订单域、用户域、营销域……

但在真正的一线高并发系统里,大厂拆服务远不止这一个维度,还会先看:

  1. 压力模型:高频高并发 vs 低频突发
  2. 主链路规划:哪些服务一挂业务就挂?
  3. 用户群体 / 前台 vs 后台场景

1. 压力模型:高频匀速 vs 低频瞬时

几个典型场景:

  • 高频 + 高并发

    • 商品详情页
    • 商品搜索
    • 营销优惠计算
  • 低频 + 瞬时高峰

    • 秒杀 / 抢购
    • 一键批量上架 / 批量改价
    • 零点库存计划发布

对高压场景的常见做法:

  • 服务隔离:单独拆出服务,独立部署、独立扩容
  • 热点隔离
    • 热点 Key 单独缓存 / 单独路由
    • 热点数据用本地缓存 + 中心缓存双层策略

这样做的目的很简单:

  • 防止高压场景「拖死」整条链路
  • 为不同服务按需分配资源(CPU/内存/带宽)

你可以回看自己系统:

  • 哪些接口 QPS 特别高?
  • 哪些操作虽然不常用,但一用就「一口气干很多」?

这些地方,都值得单独拆服务和特殊保护。

2. 主链路规划:哪些服务是「业务的命门」?

主链路是指:

用户完成一次核心业务(比如「下单」)必须经过的几步。

以电商为例:

  • 搜索 / 导流
  • 商品详情
  • 加入购物车
  • 结算页 / 优惠计算
  • 生成订单
  • 支付

主链路上的服务应该:

  • 优先拆分、优先保障可用性
  • 在高并发 / 故障场景下,有明确的降级策略
  • 在资源紧张时,其他「锦上添花」的功能要为主链路让路

比如:

  • 详情页上的「猜你喜欢」「评论列表」可以降级 / 隐藏
  • 营销价计算失败时,允许先展示原价,下单页再保障强一致

实战里,大厂会:

  • 先画一张主链路图
  • 给主链路上的每个节点拉出独立服务
  • 为这些服务单独设定:容量、限流、降级、熔断、监控

3. 用户群体 & 前台 / 后台拆分

同一业务领域下,不同用户群体的需求也差异很大:

  • C 端买家
  • B 端商家(小商家 / 大客户)
  • 内部运营 / 采购 / 风控 / 财务人员

以及:

  • 前台业务:直接面对终端用户,通常高频
  • 后台业务:运营 / 配置 / 报表,多为低频操作

拆分服务时,往往会:

  • 把前台和后台能力拆成不同服务或子域
  • 在同一领域内,根据用户群体再划一层,如:
    • 商家后台商品管理
    • 内部运营的商品审核系统
    • 前台买家的商品浏览

这样可以在:

  • 安全 / 鉴权
  • 节奏 / SLA
  • 功能复杂度

上做不同权衡。


三、无状态服务:为弹性扩缩容和高可用打地基

在微服务世界里,「无状态(Stateless)」几乎是默认要求。

什么叫有状态?

  • 服务节点上依赖本地上下文来处理请求,例如:
    • Session 存在本机内存里
    • 本地缓存里存的用户临时状态 / 热点数据,其他节点不可见

这样的问题是:

  • 一旦你加机 / 减机 / 重启 / 挂节点,
  • 路由到其他节点的请求就会「找不到状态」,造成隐性错误

1. 把状态从「节点」挪到「共享存储」

常见的「无状态化」改造包括:

  • Session → Redis / 分布式会话
  • 本地缓存仅用于提升性能不能作为唯一数据源
  • 用户 / 订单等状态信息存放在:DB / 缓存 / MQ / ES 等可共享系统

目标是:

任意请求落到任意服务节点,都可以拿到完成这次请求所需的全部状态。

2. 应用无状态 vs 配置有状态

在大规模微服务集群里,一般会:

  • 应用层:无状态

    • 多机房 / 多集群统一部署,任意节点可替换
  • 配置层:有状态

    • 不同单元 / 机房 / 集群,有不同配置(DB 地址、MQ 集群等)
    • 通过配置中心(Config Server、Diamond 等)来管理

原则是:

  • 把「和环境 / 部署有关」的东西放在配置系统
  • 把「和业务请求有关」的东西做到节点无状态

四、接口版本控制:学会优雅地兼容老客户端

微服务 + App 的组合里,一个现实问题是:

  • 你的服务迭代速度很快
  • 但用户手机上的旧版本 App 可能长期存在

如果你只保留一个最新版接口,很容易:

  • 一次改动就把老版本全部打挂
  • 升级节奏被拖死,啥都不敢改

1. 问题场景:下单接口的进化

假设你有一个下单接口,经历了三个发展阶段:

  • V1:只支持单商品下单(详情页直接买)
  • V2:支持单门店多商品下单
  • V3:支持跨店购物车、子订单拆分

如果所有版本 App 都打到同一个 /checkout 上,只在里面用 if-else 区分:

  • 代码会极其臃肿
  • 回归困难、风险极大
  • 任何小改动都可能误伤其它版本

更合理的方式是:

用「接口版本控制」把不同版本路由到不同实现上。

2. RPC 场景:利用框架的 version 字段

多数 RPC 框架(Dubbo / gRPC / HSF 等)都内置版本支持:

  • 服务提供方可以暴露多个带 version 的实现
  • 调用方在引用时指定版本:version = "1.0.0" / "2.0.0"

你可以:

  • 为 V1、V2、V3 拆出不同的 Service 接口实现
  • 在治理平台 / 配置里控制不同 App 版本调用哪一组接口

3. HTTP 场景:Path 或 Header 里带版本

两种常见方式:

  • Path 版本

    • /api/v1/checkout
    • /api/v2/checkout
    • /api/v3/checkout
  • Header 版本

    • 在请求 Header 里加 X-Api-Version: 1
    • API Gateway / 业务网关根据 Header 路由到不同后端服务

实践建议:

  • 对外开放 API / 对多端(Web / App / 第三方)使用的接口,用 Header + 网关路由更优雅
  • 内部服务间 RPC,优先用框架自带的版本控制机制

总目标是:

  • 老版本继续可用,新版本可以不被历史包袱拖死
  • 后期下线某个版本,只需要调整网关 / 注册中心,而不是「手撕大 if-else」

五、流量整形与分布式限流:先把洪水改成「可控水流」

高并发微服务系统里,流量常常不是「均匀的」,而是:

  • 某一刻(如双 11 0 点)突然冲上去
  • 某个功能突然变成爆款(热点 Key)

为了不让洪峰把后端服务一下冲垮,常见手段是:

  1. 流量整形(Token Bucket / Leaky Bucket)
  2. 网关层 + 业务层多级限流

1. 两个经典算法:令牌桶 & 漏桶

令牌桶(Token Bucket)

  • 定速往桶里放「令牌」,桶容量有限
  • 每个请求来时,从桶里取走对应数量令牌
  • 没有令牌的请求要么排队,要么被丢弃 / 降级

特点:

  • 令牌可以堆积 → 能吃掉一部分突发流量

漏桶(Leaky Bucket)

  • 请求先进入桶,桶容量有限,满了就丢弃后来的请求
  • 按固定速率从桶中「漏」出请求供后端处理

特点:

  • 把突发流量整形为「匀速」流量,后端处理得更稳定

在实际系统中,这两类思想经常和预热结合使用:

  • 系统刚启动时,QPS 上限较低,逐步拉升到目标值
  • 避免冷启动阶段突然大流量压上来

2. 网关层限流 vs 业务层限流

  • 网关层(Nginx / API Gateway)

    • 粗粒度限流:IP、全局 QPS、连接数、带宽
    • 最便宜、最早期的防线:拦在系统最外层,防止无意义流量进入
  • 业务层(Gateway / Zuul / 自研中间件)

    • 细粒度限流:按用户、按业务类型、按接口、按参数
    • 可以结合降级 / 熔断,返回更友好的结果

实现选择:

  • Nginx + Lua / OpenResty
  • Spring Cloud Gateway + Redis + Lua
  • Sentinel / 自研限流组件(Redis + Lua)

你可以在设计限流策略时问自己:

  • 哪些流量「坚决不能让进来」?放在网关层
  • 哪些流量「可以更精细地调控」?放在业务层

六、EDA 事件驱动架构:用消息把系统「松耦合」起来

在微服务里,接口调用(RPC / HTTP)不是唯一的协作方式,
事件驱动架构(EDA, Event-Driven Architecture) 在很多场景下更合适:

  • 上游只需要「发布事件」
  • 下游任何订阅者都可以各自处理,不互相耦合

典型特性:

  • 异步:不会把上游请求阻塞住
  • 松耦合:上游不需要知道有谁在听
  • 跨平台 / 跨语言:通过 MQ / 事件总线连接不同技术栈

常见应用:

  • 削峰填谷:订单写入 / 日志写入 / 审计写入等
  • 结果通知:支付结果、物流状态、账务结算
  • 最终一致性补偿:本地事务完成后,发出事件给下游补齐数据

在账务系统案例中,就可以这么设计:

  • 数据库 binlog → 增量同步工具(Canal 等)→ EventBus
  • EventBus 派发事件给:会计子系统、报表系统、退款流程等消费者

上游支付平台可以不改,甚至「不知道你存在」,
你通过监听数据变化 + 消息驱动,把账务逻辑「挂」在它后面。


七、Base 理论与幂等性:在一致性和可用性之间找平衡

最后两个理论,是微服务设计里经常被问、也经常被误解的。

1. Base 理论:基本可用 + 软状态 + 最终一致性

Base 是 CAP 理论在互联网高并发场景下的一种务实落地:

  • Basically Available(基本可用)
    • 在高压 / 降级 / 部分故障下,系统仍能提供「凑合可用」的服务
  • Soft State(软状态)
    • 系统状态在短时间内允许「不一致」,比如缓存与 DB 不同步
  • Eventually Consistent(最终一致性)
    • 在某个时间窗口之后,系统会收敛到一致状态

背后的取舍是:

  • 比起强一致性,互联网业务更在乎「可用性」
  • 一致性可以通过补偿 / 重试 / 审计,在后续慢慢修

你可以用两个例子理解:

  • 缓存一致性

    • 缓存和 DB 在极短时间内可能不一致,
    • 通过 TTL / 双删 / 定时刷新 / 热点主动重建 等手段在后续修正
  • 分布式事务

    • 很多场景用 TCC / Saga / 本地事务 + 事件通知来做最终一致
    • 而不是跨库强 2PC

2. 幂等性:多次调用,效果等于一次

在有补偿 / 重试场景下(网络超时、服务降级、消息重复投递),
幂等性是你能放心重试的前提。

定义很简单:

对同一资源的同一操作,调用一次和调用多次,业务结果应相同。

按 CRUD 分几类看:

  • Create(新增)

    • 典型场景:下单、注册
    • 方案:业务唯一键(邮箱 / 手机 / 业务 ID)+ 唯一约束 / Token 防重 / 分布式锁
  • Update(修改)

    • 典型场景:修改个人信息、订单状态推进
    • 方案:乐观锁(version 字段)、状态机(只允许特定状态迁移)
  • Delete(删除)

    • 一般天然幂等(删一次 / 多次效果一样)
    • 但互联网业务更多用「逻辑删除(软删)」+ Update,需要结合 Update 幂等性来设计
  • Query(查询)

    • 本身不改状态,一般不需要特殊幂等处理
    • 更多关注的是在最终一致性方案下的数据新鲜度 / 一致性级别

一个实用思路是:

  • 每设计一个接口,都问自己三件事:
    1)这个操作会不会被重试?
    2)重试时会不会出事?
    3)要用什么「业务 ID / Token / 锁 / 版本号」来防重复?

小结:微服务的关键不在「拆得有多细」,而在「设计得有多稳」

本章绕着微服务设计转了一圈,从:

  • 服务内分层、弱化 Controller、抽出 API 模块
  • 服务拆分维度:压力模型、主链路、领域模型、用户群体
  • 无状态服务、接口版本控制、流量整形与限流
  • 事件驱动、Base 理论、幂等性

真正有价值的是,把这些原则慢慢练成你的默认思考方式:

  • 不再看到单体就「手痒想切」,而是先看清主链路和压力模型
  • 拆服务时,不只按「对象名」划,而是按「业务边界 + 流量特征 + 团队边界」来划
  • 设计接口时,自然会考虑版本 / 幂等 / 一致性策略

当你被问到「你们的微服务是怎么设计的?」时,
如果你能围绕这些维度讲出自己项目里的真实做法和踩过的坑,
那你不仅是「用过微服务」的人,而是真正懂微服务设计的人。+