微服务的基本设计原则:别只会拆服务,要会「养服务」
微服务不是把单体照着模块名一刀一刀切开就完事了。
很多团队上微服务,第一步就是「先拆一圈 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)拆:商品域、订单域、用户域、营销域……
但在真正的一线高并发系统里,大厂拆服务远不止这一个维度,还会先看:
- 压力模型:高频高并发 vs 低频突发
- 主链路规划:哪些服务一挂业务就挂?
- 用户群体 / 前台 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 路由到不同后端服务
- 在请求 Header 里加
实践建议:
- 对外开放 API / 对多端(Web / App / 第三方)使用的接口,用 Header + 网关路由更优雅
- 内部服务间 RPC,优先用框架自带的版本控制机制
总目标是:
- 老版本继续可用,新版本可以不被历史包袱拖死
- 后期下线某个版本,只需要调整网关 / 注册中心,而不是「手撕大 if-else」
五、流量整形与分布式限流:先把洪水改成「可控水流」
高并发微服务系统里,流量常常不是「均匀的」,而是:
- 某一刻(如双 11 0 点)突然冲上去
- 某个功能突然变成爆款(热点 Key)
为了不让洪峰把后端服务一下冲垮,常见手段是:
- 流量整形(Token Bucket / Leaky Bucket)
- 网关层 + 业务层多级限流
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 理论、幂等性
真正有价值的是,把这些原则慢慢练成你的默认思考方式:
- 不再看到单体就「手痒想切」,而是先看清主链路和压力模型
- 拆服务时,不只按「对象名」划,而是按「业务边界 + 流量特征 + 团队边界」来划
- 设计接口时,自然会考虑版本 / 幂等 / 一致性策略
当你被问到「你们的微服务是怎么设计的?」时,
如果你能围绕这些维度讲出自己项目里的真实做法和踩过的坑,
那你不仅是「用过微服务」的人,而是真正懂微服务设计的人。+