微服务架构设计实战:从单体到分布式
凌晨 2:17,办公室只剩下我和空调的嗡嗡声。显示器上的日志还在刷,咖啡杯底残留着昨晚泡面的汤——别误会,不是我吃宵夜,是上周五上线微服务拆分后,连着三天排查一个诡异的分布式事务 Bug,根本没时间吃饭。
我是阿哲,在一家电商中台组做服务端开发快两年了。说“中台”其实有点心虚,毕竟我们这所谓的“中台”,两年前还是个 50 万行代码、十几个模块耦合在一起的巨型单体应用,每次发版都像在拆炸弹,产品经理提需求时我都想给他配个防爆头盔。
但今天这篇,不吐苦水,只聊干货。因为就在上个月,我们终于把那个“祖传单体”成功拆成了 8 个微服务,系统稳定性提升了一倍多,P0 级事故从月均 3 次降到 0。虽然过程堪比渡劫,但值了。这篇文章,就是想把这段实战经验和开发心得掏出来,给正在或即将踏上微服务之路的兄弟们一点参考——少踩点坑,早点下班。
为什么非拆不可?不是为了时髦
很多人以为搞微服务是为了“跟风”、“显得高大上”。说实话,我们一开始也是被逼的。
去年双11前一个月,老板拍板要上“直播带货”功能,要求两周内上线。可我们的订单、库存、用户、支付全在一个 Spring Boot 应用里,改个库存逻辑,测试得跑全量回归,CI/CD 流水线动不动就红。更离谱的是,某个营销活动接口一并发高,整个服务 CPU 直接飙到 95%,连登录都卡死。
那天晚上,运维大哥在群里甩了个 Grafana 截图,标题就俩字:“救火”。我盯着那条曲线,心里只有一个念头:再这么下去,别说双11,我人都要没了。
于是,技术总监在周会上扔下一句话:“拆!必须拆!年底前微服务化,谁拦着谁走人。”——典型的互联网公司风格,deadline 是第一生产力。
拆之前先想清楚:拆什么?怎么拆?
很多人一上来就喊“DDD!领域驱动!”,结果拆完发现服务边界模糊,调用链乱成蜘蛛网。我们团队吃过这亏。
第一步不是写代码,而是画图。
我们花了整整一周,拉着产品、测试、运维开了四轮对齐会(期间产品经理差点睡着),用 EventStorming 的方式,把核心业务流程过了一遍:用户下单 → 扣库存 → 创建订单 → 支付 → 发货 → 售后。
然后,我们按业务内聚性和数据一致性边界划出初步的服务:
user-service:用户信息、权限product-service:商品、SKU、类目inventory-service:库存扣减、回滚order-service:订单创建、状态管理payment-service:支付渠道对接notification-service:短信、站内信- ……
📌 开发心得:不要按“模块”拆,要按“业务能力”拆。比如“购物车”看似独立,但它强依赖商品和用户,初期我们把它合并进
product-service,避免跨服务事务。
工具方面,我们用了 PlantUML 画服务依赖图,Swagger 定义 API 契约,Postman 建立 Mock Server 让前端先跑起来。这些工具不是炫技,而是保证拆分过程中前后端不脱节的关键。
通信:REST 还是 RPC?我们选了 gRPC
单体时代,内部调用就是方法调用,快得很。拆成微服务后,网络延迟就成了头号敌人。
一开始我们用 Spring Cloud OpenFeign + REST,结果压测时发现,一次下单要跨 4 个服务,总耗时从 80ms 涨到 320ms。测试妹子直接在群里@我:“阿哲,你这接口是骑共享单车去请求的吗?”
痛定思痛,我们切换到 gRPC。理由很简单:
- Protobuf 序列化体积小、解析快
- HTTP/2 多路复用,减少连接开销
- 强类型接口,IDE 自动补全,少写文档
举个例子,order-service 调用 inventory-service 扣库存:
// inventory.proto
service InventoryService {
rpc DeductStock(DeductRequest) returns (DeductResponse);
}
message DeductRequest {
string sku_id = 1;
int32 quantity = 2;
string order_id = 3; // 用于幂等
}
message DeductResponse {
bool success = 1;
string error_code = 2;
}
生成的 Java Client 一行搞定调用:
InventoryServiceGrpc.InventoryServiceBlockingStub stub =
InventoryServiceGrpc.newBlockingStub(channel);
DeductResponse resp = stub.deductStock(
DeductRequest.newBuilder()
.setSkuId("SKU_123")
.setQuantity(2)
.setOrderId("ORDER_456")
.build()
);
💡 实战经验:gRPC 虽好,但调试不如 REST 方便。我们配套搭了 gRPC-Gateway,对外暴露 REST 接口,内部走 gRPC,兼顾了运维友好性和性能。
数据一致性:Saga 模式救我狗命
微服务最头疼的,莫过于分布式事务。
单体时代,@Transactional 一加,万事大吉。现在,扣库存、建订单、发通知分布在三个服务,一个失败全得回滚。XA 协议?太重。TCC?代码复杂度爆炸。
我们最终采用了 Saga 模式 + 补偿机制。
以“创建订单”为例:
order-service创建订单(状态:pending)- 调用
inventory-service扣库存- 成功 → 继续
- 失败 → 标记订单为 failed,结束
- 调用
payment-service预占支付(如冻结余额)- 成功 → 订单状态变为 confirmed
- 失败 → 触发补偿:调
inventory-service回滚库存
关键在于:每个正向操作都要有对应的反向补偿操作,且补偿操作必须幂等。
我们用 RocketMQ 事务消息来保证 Saga 步骤的可靠性。伪代码如下:
// order-service
@Transactional
public void createOrder(OrderDTO dto) {
Order order = saveOrder(dto); // DB 插入,状态 pending
// 发送事务消息,触发扣库存
rocketMQTemplate.sendMessageInTransaction(
"ORDER_CREATE_TOPIC",
MessageBuilder.withPayload(dto).build(),
order.getId()
);
}
@RocketMQTransactionListener
public class OrderTransactionListener implements RocketMQLocalTransactionListener {
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
inventoryClient.deductStock(...); // 扣库存
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
return RocketMQLocalTransactionState.ROLLBACK; // 触发补偿
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
// 查询本地订单状态,决定是否重试
return orderService.getStatus(msg.getOrderId()) == "confirmed"
? COMMIT : ROLLBACK;
}
}
⚠️ 血泪教训:补偿不是万能的!比如支付成功了但通知服务挂了,钱已经扣了,不能“退钱”。所以我们在关键路径上加了人工审核队列,让运营后台能手动干预。
配置与注册:Nacos 真香
早期我们用 Eureka + Config Server,结果配置改了要重启服务。运维大哥怒吼:“你们开发是不是觉得服务器不用电费?”
后来迁到 Nacos,真香。
- 服务注册发现:秒级上下线
- 动态配置:改个限流阈值,实时生效
- 命名空间隔离:dev / test / prod 清晰分开
配置示例(bootstrap.yml):
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: nacos.prod:8848
config:
server-addr: nacos.prod:8848
file-extension: yaml
namespace: prod-ns
配合 @RefreshScope,运行时刷新配置:
@RestController
@RefreshScope
public class ConfigController {
@Value("${app.max-retry:3}")
private int maxRetry;
@GetMapping("/config")
public String getConfig() {
return "maxRetry=" + maxRetry;
}
}
运维再也不用半夜打电话让我“赶紧重启一下服务”了——当然,他现在改催我修 Bug 了。
监控与链路追踪:没它不敢上线
微服务一拆,日志分散在 8 台机器上。以前查 Bug 是 grep,现在得拼图。
我们搭了 ELK + SkyWalking 黄金组合:
- SkyWalking:自动埋点,看调用链、拓扑图、慢接口
- ELK:集中日志,用
traceId串起整条链路
效果有多爽?上周五,用户反馈“下单超时”,我在 SkyWalking 里搜订单号,3 秒定位到是 payment-service 调第三方支付网关超时,直接联系支付渠道,省了两小时抓包。
📊 性能对比(拆分前后)
| 指标 | 单体架构 | 微服务架构 |
|---|---|---|
| 平均响应时间(下单) | 220ms | 95ms |
| P99 延迟 | 1.8s | 420ms |
| 日发布次数 | 1~2 次 | 10+ 次 |
| 故障隔离能力 | 无(全挂) | 单服务故障不影响全局 |
总结:微服务不是银弹,但值得折腾
回过头看,这场拆分就像一场大型手术。过程中我们踩过无数坑:
- 忘了加熔断,一个服务雪崩拖垮全链路
- ID 生成器没统一,订单号重复(还好没丢钱)
- 本地缓存没清理,用户看到旧库存……
但每一次深夜 debug 后的“终于通了”,都让我觉得这行没白干。
微服务的核心不是技术,而是协作方式的升级。它逼你写清晰的接口契约,做完善的监控告警,考虑服务的自治性。这些,恰恰是高质量代码的基石。
现在,我依然经常加班到凌晨。但至少,我不再害怕改一行代码就炸掉整个系统了。而且,听说隔壁组也在准备拆,产品经理最近看我的眼神都带着敬畏——大概以为我会写魔法吧。
共勉。希望你读完这篇,能少熬几个夜,多陪陪家人。
最后一句真心话:别为了微服务而微服务。如果你的系统还没到“改一处崩全局”的地步,老老实实用单体,早点回家吃饭,它不香吗?

评论 0