微服务架构设计实战:从单体到分布式
引言:为什么我会踏上微服务的转型之路?

还记得2019年那会儿,我们公司的一个核心系统还是一套庞大的Java单体应用。这个系统承载着整个公司的核心业务流程,包括订单、库存、支付等等。最初它运行得还不错,但随着功能越来越多、团队规模越来越大,维护起来越来越吃力。
代码库像滚雪球一样膨胀,每次上线都要小心翼翼地做回归测试,生怕改动一个地方就牵一发动全身。更头疼的是,新功能开发周期越来越长,一个小需求可能要在多个模块之间来回修改,牵扯到好几个小组之间的协作。
我们尝试过模块化拆分,把不同业务划成不同的子模块,但本质上还是跑在一个JVM里,部署还是单一节点。这种“伪模块化”并没有真正解决问题,反而让依赖管理变得更加复杂。
当时我就意识到,如果不做出改变,这棵大树迟早会被自己的枝叶压垮。
于是,在一次技术评审会上,我提出了一个大胆的想法:“要不咱们试试微服务?”
项目背景:从单体走向分布式的真实动机

这套系统是为一家中型连锁零售企业打造的后台管理系统,覆盖线上商城、门店POS系统、供应链、仓储等模块。系统用户量不算大,日活跃大概在几千人左右,但每个请求都涉及到多个业务逻辑操作,且对稳定性要求非常高。
当时的痛点主要包括:
- 部署难:任何小改动都需要整个系统重启
- 扩展性差:某个模块出现性能瓶颈时无法单独扩容
- 开发效率低:多人协作导致频繁的代码冲突和版本问题
- 故障隔离差:一处出错整个系统瘫痪
这些问题累积下来,最终促使我们在2020年初启动了微服务架构的重构计划。
遇到的挑战:现实远比设想复杂得多

虽然网上有很多微服务的成功案例,但真到了自己动手的时候才发现,理论和实践之间的差距不是一点点。
第一个坑:拆分边界怎么定?
刚开始我们很迷茫,到底该怎么拆?是按业务来分,比如订单服务、用户服务、库存服务?还是按照层级来划分,比如DAO层、业务层、API层?
这个问题讨论了很久,最后我们参考了DDD(领域驱动设计)的思想,以业务能力为中心进行划分。例如:
- 订单相关功能统一归为订单服务
- 用户信息、权限、登录注册统一归为用户服务
- 库存查询、出入库操作归为库存服务
但在初期实践中,我们发现有些业务边界并不清晰。比如订单中涉及库存的操作应该如何处理?是调用库存服务API,还是直接访问数据库?
我们尝试了一段时间跨服务调用,结果发现响应时间变慢很多。后来决定采用“数据冗余+异步同步”的方式,既保证响应速度,又避免强耦合。
第二个坑:服务间通信怎么做?
一开始我们选择了Spring Cloud Feign作为服务间通信的方式,简单方便,和Eureka集成也很顺畅。
然而不久后遇到了两个大问题:
- Feign客户端配置不当导致请求超时
- 服务雪崩效应
第一个问题是由于Feign默认的超时时间太短(连接1s,读取1s),而我们的某些服务在高峰期响应本身就比较慢,导致大量请求失败。
解决办法是在application.yml中加上如下配置:
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 10000
第二个问题则出现在某个服务出现延迟或宕机时,请求堆积在网关,造成连锁反应。我们引入了Hystrix作为熔断器,但后来发现它的线程池模型在高并发下表现不佳,最终换成了Resilience4j,轻量级、响应式更好。
第三个坑:数据一致性如何保障?
微服务的最大挑战之一就是分布式事务的问题。比如下单场景,需要同时扣减库存、生成订单、扣除用户积分等多个操作。
我们尝试过几种方案:
- 本地事务 + 最终一致性:先保证本地事务成功,再通过MQ异步通知其他服务执行后续操作
- TCC补偿事务:定义Try、Confirm、Cancel三个阶段,适合金融类强一致场景
- Saga模式:适用于链路较长的流程型业务,出错时通过逆向操作回退
最终我们选择了前两种结合使用。对于高并发下的非关键路径使用消息队列异步处理,而对于资金相关的操作则采用TCC,确保最终一致性。
技术选型与架构设计
为了支撑微服务系统的顺利运行,我们需要搭建一套完整的基础设施。我们的技术栈主要包括:
| 组件 | 技术选型 |
|---|---|
| 注册中心 | Nacos |
| 网关 | Spring Cloud Gateway |
| 服务通信 | OpenFeign + Resilience4j |
| 配置中心 | Nacos |
| 分布式事务 | Seata |
| 数据库 | MySQL + MyCat(分库分表) |
| 日志聚合 | ELK |
| 监控告警 | Prometheus + Grafana + SkyWalking |
下面是整体架构图的一部分(文字描述):
外部请求 -> API Gateway -> 各个微服务(订单、用户、库存)
↓
调用其他服务 或 操作数据库
↓
异步通知 via RocketMQ
网关的设计考虑
我们最初使用Zuul作为网关,但在实际使用中发现性能不够好,尤其是在高并发情况下延迟较高。后来切换到Gateway之后,性能提升明显。
一些关键配置示例:
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- StripPrefix=1
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/user/**
网关层面我们也做了限流、鉴权、日志追踪等功能,避免重复工作分散到各个服务中去。
关键代码片段分享
服务间通信示例(Feign Client)
@FeignClient(name = "inventory-service", path = "/api/inventory")
public interface InventoryServiceClient {
@PostMapping("/deduct")
ResponseDTO deductStock(@RequestParam("productId") Long productId,
@RequestParam("quantity") Integer quantity);
}
这里我们封装了对库存服务的调用,并配合Resilience4j做降级:
@Bean
public InventoryServiceClient fallbackInventoryClient() {
return (productId, quantity) -> new ResponseDTO(false, "服务暂不可用,请稍后再试");
}
分布式事务 TCC 示例
以库存服务为例,定义TCC接口:
public interface InventoryTccAction {
@TwoPhaseBusinessAction(name = "deductStock")
boolean prepare(BusinessActionContext ctx, @ActionContextParam("productId") Long productId,
@ActionContextParam("quantity") Integer quantity);
@Commit
boolean commit(BusinessActionContext ctx);
@Rollback
boolean rollback(BusinessActionContext ctx);
}
实现部分会在prepare阶段检查并锁定库存,在commit时正式扣除,在rollback时释放锁定数量。
生产踩坑经验总结
1. 服务注册与发现不稳定
早期我们使用Eureka作为注册中心,但在实际生产中遇到过服务实例未及时下线的问题,导致调用失败。
解决方案是我们转用了Nacos,它不仅支持服务注册与发现,还能作为配置中心,同时提供了健康检查机制,实时性更强。
2. 跨域问题层出不穷
在网关和服务之间转发请求时,我们曾因CORS设置不规范而导致前端请求被浏览器拦截。
我们在网关层统一设置了全局CORS策略:
@Configuration
public class CorsConfig {
@Bean
public WebFilter corsFilter() {
return (ServerWebExchange exchange, WebFilterChain chain) -> {
ServerHttpResponse response = exchange.getResponse();
ServerHttpRequest request = exchange.getRequest();
if (request.getHeaders().containsKey(HttpHeaders.ORIGIN)) {
response.getHeaders().add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
response.getHeaders().add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS");
response.getHeaders().add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type, Authorization");
response.getHeaders().add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
}
return chain.filter(exchange);
};
}
}
3. 日志追踪混乱
多个服务产生的日志混在一起,排查问题非常困难。我们引入了SkyWalking来做全链路追踪,为每个请求分配唯一traceId,并将日志输出到ELK。
4. 数据库压力过大
最初我们把所有服务共用一个MySQL数据库,结果很快出现锁竞争严重、QPS下降等问题。
后来我们做了三件事:
- 每个服务独立数据库
- 使用MyCat做垂直拆分
- 热点数据引入Redis缓存
效果立竿见影,数据库响应时间从平均80ms降到15ms以内。
效果总结:改造后的收益与提升
经历了大半年的微服务重构和优化,我们取得了以下成果:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 部署频率 | 2周/次 | 每天可多次发布 |
| 服务可用性 | 98% | 99.8% |
| 新功能上线时间 | 平均2周 | 平均3天 |
| 单机QPS | ~200 | ~1500 |
| 故障影响范围 | 全系统 | 仅受影响服务 |
最重要的是,团队协作更加顺畅了,前后端分离更明确,测试和运维也能针对性地管理。
我的经验建议:给正在转型的你一点启发
如果你也正准备或正在进行微服务架构的升级,我可以分享几个关键建议:
1. 不要盲目追求“微”
并不是服务越多越好。合理的拆分粒度非常重要,建议优先按业务边界来划分,而不是为了拆而拆。初期可以先拆成3~5个核心服务,后续逐步细化。
2. 基础设施先行
在开始拆服务之前,一定要先准备好基础平台:
- 服务注册与发现(Nacos / Consul)
- 网关(Spring Cloud Gateway / Zuul)
- 配置中心(Nacos / Spring Cloud Config)
- 日志监控(ELK + SkyWalking)
- 分布式事务组件(Seata / TCC)
没有这些底座支持,服务多了只会带来更大的混乱。
3. 接口设计比代码更重要
服务间的通信必须要有良好的契约设计。推荐使用OpenAPI标准,提前做好文档说明和版本控制。不要等到上线前才临时改接口。
4. 重视异常处理和熔断机制
微服务天然存在网络不可靠的问题,所以必须处理好超时、重试、熔断、降级等机制。否则一个小故障可能拖垮整条调用链。
5. 数据治理不能忽视
微服务意味着数据库拆分,这时候更要考虑数据一致性、迁移、备份、容灾等问题。建议尽早规划主从复制、异地多活等方案。
写在最后:这场转型带来的不仅仅是技术变化
对我来说,这次微服务的落地不仅仅是一次技术升级,更是一次思维的转变。它让我重新理解了系统设计的初衷——不是为了炫技,而是为了让系统更好地服务于业务,服务于团队。
记得有一次凌晨值班,有个服务突然出现OOM(Out of Memory),我第一反应是去查堆栈、看GC日志、调整JVM参数……折腾了好几个小时才定位到是一个定时任务内存泄漏。那时候我才明白,分布式的世界,运维的压力同样巨大。
但正是这一次次的“坑”,一次次的“救火”,让我和团队一步步成长。如今回头看,那段日子虽然辛苦,却也无比充实。
希望这篇文章能给你一点启发。如果你正在做类似的事情,别怕困难,坚持走下去。因为每一步微服务的改进,都在让系统变得更健壮、更灵活、更有生命力。
如果你觉得这篇文章有帮助,欢迎留言交流,或关注我的技术博客。让我们一起在微服务的道路上越走越远!

评论 0