微服务架构设计实战:一次从单体到分布式的蜕变之旅

Bug自己会好
2025-06-18 02:57
阅读 605

背景:为什么要做这件事?

背景:为什么要做这件事?

大概三年前,我加入了一个处于快速成长期的电商项目组。当时的系统是一个标准的“大单体”——用 Spring Boot + MyBatis 构建的后端,所有业务逻辑都混在一起,代码仓库超过百万行,编译时间越来越长,部署越来越慢。

每次上线都像走钢丝:改一个接口可能影响整条业务链,测试覆盖不全面,线上故障频发。更糟的是,随着业务功能越来越多,团队协作也越来越困难。我们几个开发者经常因为合并冲突、依赖混乱而浪费大量时间。

老板说:“是时候重构了。”但我们知道,简单的代码结构优化已经撑不了多久,唯一可行的路径就是——拆!

于是,我们决定从零开始,把整个系统迁移到微服务架构。这不仅是一次技术上的变革,更是一次组织协同和开发流程的大改造。


问题描述:拆的过程中到底遇到了啥坑?

问题描述:拆的过程中到底遇到了啥坑?

刚上手那会儿,我们都兴奋不已,觉得只要把模块拆开、跑起来就万事大吉了。结果理想丰满,现实骨感。真正拆的过程中,各种问题扑面而来:

1. 服务边界划分模糊

最初我们按照模块粗略地切分:用户服务、订单服务、商品服务。但很快发现,用户服务里还要处理会员积分、优惠券发放等逻辑;订单服务又需要获取商品信息、库存状态,还有优惠策略的计算……

你中有我、我中有你的情况太严重,拆了个寂寞。

2. 数据库共享引发的问题

为了图方便,初期多个服务共享同一个数据库,只是通过不同 schema 或前缀区分表。结果数据耦合越来越重,一个服务出错可能导致整个库锁死。更麻烦的是,事务控制成了噩梦,比如下单扣库存操作跨两个服务怎么搞?本地事务根本解决不了。

3. 通信方式的选择纠结

一开始我们用 HTTP 同步调用,后来发现调用链过长时失败率飙升,超时、熔断、重试这些机制没跟上,导致系统崩溃。后来尝试引入消息队列异步解耦,却发现消费者消费延迟严重,数据一致性难保障。

4. 部署与运维复杂度陡增

原本单个 jar 包就可以部署的应用,现在变成了十几个服务实例,加上 Gateway、配置中心、注册中心、监控系统……运维同事直呼受不了:“以前升级一次5分钟搞定,现在得花半小时。”

5. 开发调试变困难

本地开发要启动一堆服务,还要连远程数据库或中间件,调试变得异常繁琐。有时候一个小改动就要等半天服务重启。


解决方案:我们的拆法和选型思路

经过一段时间的踩坑和摸索,我们逐步建立起一套适合自己业务场景的微服务架构体系。下面是我们最终采用的核心设计方案和技术栈(以 Java 为主):

架构图概览

[前端] → [API Gateway] → [Auth Service, User Service, Order Service, Product Service...]
                            ↓ 
              [Event Bus: Kafka/RabbitMQ] 
                            ↓ 
                     [Log/Alarm/Monitoring]

技术选型一览

组件 选型方案
注册中心 Nacos
配置中心 Spring Cloud Config + Git
网关 Spring Cloud Gateway
通信方式 Feign / WebFlux + Reactor
异步消息 Kafka
数据库 PostgreSQL + 分库分表
分布式事务 Seata(AT 模式为主)
监控追踪 Prometheus + Grafana + Zipkin
日志收集 ELK Stack

实践篇:关键代码片段和实现思路

一、服务拆分边界定义思路

我们将原来的单体应用按领域驱动的方式重新划分(DDD),比如:

  • 用户域:登录注册、权限管理、会员等级、积分
  • 商品域:商品管理、类目、SKU 管理
  • 订单域:下单、支付、退换货、售后
  • 库存域:库存管理、库存变动、预警规则

每个域拥有自己独立的数据模型和 API 接口,对外仅暴露核心服务接口。

// 示例:订单服务对外接口
public interface IOrderService {
    Order createOrder(String userId, List<Item> items);
    Order getOrderById(String orderId);
    void cancelOrder(String orderId);
}

@Service
public class OrderServiceImpl implements IOrderService {
    private final IProductServiceClient productService;
    
    // 通过 Feign 远程调用商品服务判断库存是否足够
    @Override
    public Order createOrder(String userId, List<Item> items) {
        for (Item item : items) {
            Product product = productService.getProduct(item.productId());
            if (product.getStock() < item.quantity()) {
                throw new InsufficientStockException();
            }
        }
        // 创建订单,更新数据库...
    }
}

注: Feign Client 是这样声明的:

@FeignClient(name = "product-service", configuration = FeignConfig.class)
public interface IProductServiceClient {
    @GetMapping("/products/{id}")
    Product getProduct(@PathVariable("id") String productId);
}

微服务架构示意图-1

二、分布式事务处理方案(Seata)

针对下单、扣库存的强一致性需求,我们在部分场景下使用了 Seata 的 AT 模式,例如:

// 主订单服务中开启全局事务
@GlobalTransactional
public void processOrder(Order order) {
    inventoryService.deductInventory(order.getItems());
    paymentService.processPayment(order.getUserId(), order.getTotalPrice());
    order.setStatus("paid");
    orderRepository.save(order);
}

inventory-service 中:

@Override
@TwoPhaseBusinessAction(name = "deductInventory")
public boolean deduct(BusinessActionContext ctx, DeductRequest request) {
    // 扣减库存操作
    return true;
}

@Commit
public boolean commit(BusinessActionContext ctx) {
    return true;
}

当然,在实际中并不是所有场景都强制一致性,有些我们采用了最终一致性 + 补偿机制,比如优惠券发放失败则记录日志并异步重试。

三、日志统一和链路追踪配置(Zipkin)

在每一个服务的入口加如下拦截器,记录请求耗时和服务间调用关系:

@Configuration
@EnableWebFlux
public class TraceConfiguration implements WebFluxConfigurer {
    
    @Bean
    public WebFilter tracingWebFilter(Tracer tracer) {
        return (exchange, chain) -> {
            String traceId = UUID.randomUUID().toString();
            String spanId = UUID.randomUUID().toString();
            
            exchange.getRequest().mutate()
                .header("X-B3-TraceId", traceId)
                .header("X-B3-SpanId", spanId)
                .build();

            tracer.createSpan(exchange.getRequest().getURI().getPath());
            return chain.filter(exchange).doOnTerminate(tracer::closeSpan);
        };
    }
}

application.yml 中配置 Zipkin 地址即可自动上传:

spring:
  zipkin:
    base-url: http://zipkin-server:9411

踩过的坑和经验教训总结

坑点一:不要一开始就追求完美架构

最开始我们试图一口气搭建完所有的基础设施(网关、注册中心、链路追踪、日志收集、配置中心),结果光搭环境就花了两周多,还没开始写业务代码。后来我们调整策略,先完成主流程拆分,再逐步引入其他组件。

建议: 先小范围试点,验证可行性后再全面铺开。

坑点二:服务粒度过细反而增加维护成本

有段时间我们甚至把“生成商品推荐列表”这种逻辑单独拆成一个 service,结果发现这个服务几乎没人调用,反而带来额外负担。后来我们把这些低频率、轻量级的功能集成回原有服务中。

建议: 不要盲拆,关注服务之间的交互频率和依赖关系。

坑点三:异步不是万能药,需合理使用

我们早期对 Kafka 上瘾,恨不得一切东西都异步。结果某些需要强反馈的操作(如退款通知用户),异步导致用户投诉“钱退了但没收到通知”。最后还是回归到了同步返回 + 异步补偿结合的方式。

建议: 明确哪些业务必须实时性,哪些可以异步。

坑点四:跨服务调用失败怎么办?

比如 A 服务调用 B 服务的接口失败,此时应该:

  • 自动重试?
  • 返回错误?
  • 记录日志然后人工干预?

我们一开始没想清楚这些问题,结果线上出现很多“卡住”的订单。后来我们建立了一套“失败降级 + 回调机制”,对于不能立即处理的业务,打上标记,后续异步重试。


改造后的效果如何?

改造完成后,整体效果还是非常明显的:

对比项 单体阶段 微服务阶段
部署效率 每次发布需要停机,风险高 各服务可独立部署,滚动更新
故障隔离能力 出现问题全站瘫痪 某个服务异常不影响其他模块
开发协作效率 多人修改一个工程,冲突频繁 每个服务由小组独立维护
性能和资源利用率 冷热功能混在一个进程中浪费内存 各服务按需扩容,资源利用率提升
监控排查能力 出问题只能靠日志定位 能精确到服务、接口、链路的维度

虽然前期投入成本不小,但从长期看,这次重构为公司带来了可持续发展的能力。


给正在考虑微服务化的朋友们几点建议

1. 先诊断现状,再决定是否拆分

如果你的应用还没达到一定规模、团队人数不多、业务复杂度不高,那就没必要折腾微服务。单体架构也有它的优势——简单、易维护、部署快。

2. 服务划分宁可少也不能乱

服务边界的划分远比你想的复杂。别一上来就拆几十个服务,容易把自己绕进去。推荐从小处着手,比如先把核心业务抽象出来。

3. 基础设施必须跟上

拆完服务如果没有配套的注册中心、配置中心、监控平台,你会很痛苦。建议在正式开始之前先准备基础框架。

4. 接口文档一定要规范清晰

服务多了之后,相互之间调用必须依赖清晰的接口文档(Swagger、Postman、OpenAPI)。否则,一个字段变更就能搞崩另一个服务。

5. 提前制定好应急机制

微服务环境下,网络不稳定、接口报错是常态。必须设计好降级策略、熔断机制、重试策略、回调补偿等措施。


结语:从“写代码”到“做架构”的转变

说实话,刚开始转型微服务的时候,我还只是一个普通的后端工程师。但在这一过程中,我逐渐学会了从更高的视角去思考系统的设计、性能瓶颈、团队协作方式。

这段经历让我明白:一个好的架构不是一蹴而就的,也不是照搬理论书上的模板,而是从真实业务出发,不断迭代、打磨出来的。它既要满足当下的需求,又要对未来的变化留有空间。

如果你也正走在从单体向微服务演进的路上,希望这篇文章能给你一些启发和帮助。记得一句话:“架构不是用来炫技的,是用来解决问题的。”

共勉。


🚀本文作者:一名热爱架构和技术分享的后端程序员,经历过数十个项目的迭代重构,专注Java生态与微服务架构实践。欢迎关注公众号【码上架构】,一起探索后端技术的世界。

评论 0

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