微服务架构设计实战:从单体到分布式

代码自留地
2025-06-24 22:49
阅读 264

引言:为什么我要写这篇文章?

引言:为什么我要写这篇文章?

去年年初,我带领一个技术团队,负责公司核心系统的一次重大重构——从一个庞大的单体应用向微服务架构转型。这个系统承载着我们产品最重要的用户注册、订单交易和结算逻辑。上线多年,代码库已超过200万行,部署一次要花20分钟以上,出了问题定位异常困难。

当时我们遇到一个很现实的问题:功能迭代越来越慢,每次修改一个小地方都要全量发布;线上一旦出现故障,影响面极大;性能瓶颈开始显现,数据库连接数经常打满,请求延迟陡增。

于是我们决定拆分服务,尝试采用更现代的微服务架构,目标是提升可维护性、可扩展性和系统的稳定性。这篇文章不是教你“微服务是什么”、“CAP定理讲什么”的科普文,而是结合我过去一年真实的落地经验,聊聊我们在实践中踩过的坑、摸索出的经验以及一些关键的设计与实现思路。


项目背景:从“巨石”出发

项目背景:从“巨石”出发

我们的项目原本是一个典型的Spring Boot单体应用,基于MySQL + Redis构建,前后端分离。业务模块包括:

  • 用户中心(注册、登录、权限)
  • 订单中心(下单、支付、售后)
  • 商品中心(商品详情、库存管理)
  • 营销中心(优惠券、活动规则)

所有业务都部署在一台服务器上,虽然有通过Controller层划分不同模块,但在代码层面和数据表层面严重耦合。随着用户增长,服务响应变慢,数据库锁争抢频繁,运维成本高企。

这时候我们意识到:“不能再拖了。”

于是我们开始了一个为期6个月的服务拆解计划,目标是以微服务形式重新组织系统结构,让每个核心功能都可以独立部署、独立演进。


拆分挑战:如何正确拆分业务边界?

第一个问题:按什么维度拆分?

初期我们设想按模块拆,比如用户服务、订单服务。但很快发现这种拆分方式并不足够清晰,很多公共逻辑(如权限校验、事务控制)会被多个服务重复实现。

后来我们调整策略,改为以“领域模型”为基础进行拆分。例如:

  • 用户服务:处理与用户身份相关的操作
  • 订单服务:处理订单创建、状态变更、支付流水等
  • 库存服务:处理库存扣减、补货等
  • 消息中心服务:用于统一发送通知、短信、邮件

这种方式让我们能更好地定义领域边界,并避免过度共享。

第二个问题:数据怎么拆?

这是最头疼的地方。原来一个orders表里包含了用户ID、商品信息、地址、支付记录……现在这些信息需要归属到各个服务中。

我们采取了以下方式:

  1. 数据去耦:每个服务只持有自身关心的数据。
  2. 跨服务查询用API调用:比如用户服务提供 /user/info/{uid} 接口供其他服务使用。
  3. 最终一致性保证:使用异步队列同步部分非实时数据。

举个例子,订单服务保存的是订单ID、用户ID、价格、状态等基本信息;而具体用户信息由用户服务存储并提供接口。这样虽然增加了调用次数,但也避免了数据冗余导致的不一致问题。


技术方案选型与实践

数据流转过程-1

我们采用的技术栈如下:

  • Spring Cloud Alibaba:做服务发现(Nacos)、配置中心、熔断降级(Sentinel)
  • MyBatis Plus:简化数据库访问
  • RocketMQ:异步通信与事件驱动
  • Docker + Kubernetes:容器化部署
  • Prometheus + Grafana:监控指标
  • ELK:日志收集与分析

关键组件介绍

1. 注册中心选择:Nacos

最初我们考虑Eureka,但考虑到我们要集成阿里巴巴生态的技术栈,最终选择了Nacos。它不仅支持服务发现,还提供了动态配置管理,非常方便。

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.1.10:8848

启动后,服务自动注册到Nacos,其他服务通过OpenFeign进行远程调用。

2. 熔断限流:Sentinel + OpenFeign

为了防止雪崩效应,我们为每个服务间的调用设置了限流和熔断规则。比如,在订单服务中调用用户服务时:

@FeignClient(name = "user-service", fallbackFactory = UserServiceFallback.class)
public interface UserServiceClient {

    @GetMapping("/user/info/{uid}")
    Result<UserDTO> getUserInfo(@PathVariable Long uid);
}

@Component
public class UserServiceFallback implements FallbackFactory<UserServiceClient> {
    @Override
    public UserServiceClient create(Throwable cause) {
        return new UserServiceClient() {
            @Override
            public Result<UserDTO> getUserInfo(Long uid) {
                return Result.fail("调用用户服务失败:" + cause.getMessage());
            }
        };
    }
}

同时在Sentinel Dashboard中配置QPS限制,保障系统整体可用性。

3. 数据库拆分:多数据源 + 分布式事务

每个服务都拥有自己的数据库实例,我们使用Seata来处理分布式事务场景。

比如在下单流程中,需要同时:

  • 扣库存(库存服务)
  • 创建订单(订单服务)
  • 更新用户余额(用户服务)

这三者的操作要么全部成功,要么全部回滚。

@GlobalTransactional
public void placeOrder(PlaceOrderDTO dto) {
    // 调用库存服务减库存
    inventoryService.decreaseStock(dto.getProductId(), dto.getCount());

    // 创建订单
    orderService.createOrder(dto);

    // 修改用户余额
    userService.deductBalance(dto.getUserId(), dto.getTotalPrice());
}

虽然引入了Seata这样的中间件,但它确实解决了实际问题,尤其是在金融类或高并发电商系统中,非常有必要。


代码实践:几个关键片段分享

1. 服务调用封装

我们在调用方统一包装了异常处理逻辑,以提高容错能力:

public <T> Result<T> safeCall(Callable<Result<T>> serviceCall) {
    try {
        return serviceCall.call();
    } catch (Exception e) {
        log.error("调用外部服务失败", e);
        return Result.fail("外部服务调用异常");
    }
}

2. 配置文件热加载(Nacos)

我们将某些通用配置抽取出来集中管理:

# application.yml
spring.cloud.nacos.config.server-addr=192.168.1.10:8848
spring.application.name=order-service

然后在配置中心新建一个 order-service.yaml 文件,里面存放:

order:
  timeout: 3000
  retry-times: 3

Java 中注入即可动态获取:

@Value("${order.timeout}")
private int timeout;

配合RefreshScope注解可以实现实时刷新。


踩坑经验总结

数据流转过程-2

坑一:数据一致性问题

初期我们没有引入分布式事务,结果出现了订单已创建但库存未扣减的情况。这个问题暴露之后,我们立即引入了Seata,虽然带来了额外开销,但在关键路径上值得投入。

坑二:网络延迟+串行调用造成性能下降

一开始我们是链式调用多个服务,比如A调B,B调C。结果在线上压测时发现整体耗时暴涨。

解决办法:采用聚合服务前端调用组合的方式,减少远程调用链条。例如将用户+订单信息整合在一个Gateway服务中返回给前端。

坑三:日志追踪难

服务一多,调试就变得困难。我们通过引入SkyWalking实现了全链路追踪,结合Trace ID快速定位问题。

坑四:本地测试环境难以搭建完整依赖

我们后来采用了Docker Compose + Minikube模拟整个微服务环境,便于本地调试。


实施效果与收益

经过半年的努力,我们完成了核心模块的微服务化改造。成果如下:

指标 改造前 改造后
单次部署时间 20min <5min
故障影响范围 全站瘫痪 仅局部不可用
新功能上线周期 2~3周 3~5天
线上错误率 ~5% ~1%
同时在线用户数支撑 5w 20w

最重要的是,团队的技术架构能力得到了显著提升。开发同学不再对“动哪怕哪”感到恐惧,新功能开发更加聚焦于当前模块。


经验分享与建议

如果你也在考虑或者刚开始微服务改造,我给你几点真诚的建议:

1. 先治理好现有代码再拆分

别想着“拆成微服务就能解决一切”,如果原来的单体应用本身就有大量坏味道(大函数、重复代码、职责不清),拆出来的可能是“多个小垃圾堆”。

2. 做好服务边界的梳理

边界比代码更重要。不要一开始就想着拆几十个服务,先拆核心业务模块,逐步演进。

3. 别忽略监控与日志

微服务意味着更多节点、更多调用链,不配上一套好的监控体系,出了问题基本等于盲人摸象。

4. 异步解耦很重要

合理使用消息队列(如RocketMQ、Kafka),把一部分同步调用变成异步处理,既缓解压力也提高系统吞吐。

5. 关注性能与可观测性

服务拆得越多,潜在的性能损耗越大。比如HTTP调用本身的延迟、序列化反序列化的开销、日志收集带来的资源占用,都要提前评估。


结语:一场修行,值得坚持

微服务架构从来都不是银弹,它带来的是更灵活的架构能力,同时也伴随着更高的复杂度。这场从“巨石”到“分布式星群”的旅程,对我来说是一次技术视野的拓宽,也是一次团队协作方式的升级。

在整个过程中,我们经历了争吵、重构、重写,也有深夜的紧急修复和一次次灰度发布。但每当看到新功能可以在不影响其他模块的情况下稳定上线,我就觉得这一切都值了。

最后想说一句:架构之美不在炫技,而在恰如其分地解决问题。希望这篇文章能为你少走一些弯路提供一点启发。

感谢你读到这里,如果你有任何疑问或想交流经验,欢迎随时联系我。咱们一起成长,一路同行。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝