一,认识微服务

1.1 单体架构

单体架构(monolithic structure):顾名思义,整个项目中所有功能模块都在一个工程中开发;项目部署时需要对所有模块一起编译、打包;项目的架构设计、开发模式都非常简单。

2953321-20250423172411186-126219745.png

当项目规模较小时,这种模式上手快,部署、运维也都很方便,因此早期很多小型项目都采用这种模式。

但随着项目的业务规模越来越大,团队开发人员也不断增加,单体架构就呈现出越来越多的问题:

  • 团队协作成本高:试想一下,你们团队数十个人同时协作开发同一个项目,由于所有模块都在一个项目中,不同模块的代码之间物理边界越来越模糊。最终要把功能合并到一个分支,你绝对会陷入到解决冲突的泥潭之中。
  • 系统发布效率低:任何模块变更都需要发布整个系统,而系统发布过程中需要多个模块之间制约较多,需要对比各种文件,任何一处出现问题都会导致发布失败,往往一次发布需要数十分钟甚至数小时。
  • 系统可用性差:单体架构各个功能模块是作为一个服务部署,相互之间会互相影响,一些热点功能会耗尽系统资源,导致其它服务低可用。

1.2 微服务

微服务架构,首先是服务化,就是将单体架构中的功能模块从单体应用中拆分出来,独立部署为多个服务。同时要满足下面的一些特点:

  • 单一职责:一个微服务负责一部分业务功能,并且其核心数据不依赖于其它模块。
  • 团队自治:每个微服务都有自己独立的开发、测试、发布、运维人员,团队人员规模不超过10人
  • 服务自治:每个微服务都独立打包部署,访问自己独立的数据库。并且要做好服务隔离,避免对其它服务产生影响

例如,示例商城项目,我们就可以把商品、用户、购物车、交易等模块拆分,交给不同的团队去开发,并独立部署:

2953321-20250423193313418-1946330908.png

那么,单体架构存在的问题有没有解决呢?

  • 团队协作成本高?
    • 由于服务拆分,每个服务代码量大大减少,参与开发的后台人员在1~3名,协作成本大大降低
  • 系统发布效率低?
    • 每个服务都是独立部署,当有某个服务有代码变更时,只需要打包部署该服务即可
  • 系统可用性差?
    • 每个服务独立部署,并且做好服务隔离,使用自己的服务器资源,不会影响到其它服务。

综上所述,微服务架构解决了单体架构存在的问题,特别适合大型互联网项目的开发,因此被各大互联网公司普遍采用。大家以前可能听说过分布式架构,分布式就是服务拆分的过程,其实微服务架构正式分布式架构的一种最佳实践的方案。

当然,微服务架构虽然能解决单体架构的各种问题,但在拆分的过程中,还会面临很多其它问题。比如:

  • 如果出现跨服务的业务该如何处理?
  • 页面请求到底该访问哪个服务?
  • 如何实现各个服务之间的服务隔离?

1.3 SpringCloud

微服务拆分以后碰到的各种问题都有对应的解决方案和微服务组件,而 SpringCloud 框架可以说是目前 Java 领域最全面的微服务组件的集合了。Spring Cloud

2953321-20250423193406614-1330952087.png

而且 SpringCloud 依托于 SpringBoot 的自动装配能力,大大降低了其项目搭建、组件使用的成本。对于没有自研微服务组件能力的中小型企业,使用 SpringCloud 全家桶来实现微服务开发可以说是最合适的选择了!

二,微服务拆分

服务拆分一定要考虑几个问题:

  • 什么时候拆?
  • 如何拆?

2.1 什么时候拆

一般情况下,对于一个初创的项目,首先要做的是验证项目的可行性。因此这一阶段的首要任务是敏捷开发,快速产出生产可用的产品,投入市场做验证。为了达成这一目的,该阶段项目架构往往会比较简单,很多情况下会直接采用单体架构,这样开发成本比较低,可以快速产出结果,一旦发现项目不符合市场,损失较小。

如果这一阶段采用复杂的微服务架构,投入大量的人力和时间成本用于架构设计,最终发现产品不符合市场需求,等于全部做了无用功。

所以,对于大多数小型项目来说,一般是先采用单体架构,随着用户规模扩大、业务复杂后再逐渐拆分为 微服务架构。这样初期成本会比较低,可以快速试错。但是,这么做的问题就在于后期做服务拆分时,可能会遇到很多代码耦合带来的问题,拆分比较困难(前易后难)。

而对于一些大型项目,在立项之初目的就很明确,为了长远考虑,在架构设计时就直接选择微服务架构。虽然前期投入较多,但后期就少了拆分服务的烦恼(前难后易)。

2.2 怎么拆

之前我们说过,微服务拆分时粒度要小,这其实是拆分的目标。具体可以从两个角度来分析:

  • 高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高。
  • 低耦合:每个微服务的功能要相对独立,尽量减少对其它微服务的依赖,或者依赖接口的稳定性要强。

高内聚首先是单一职责, 但不能说一个微服务就一个接口,而是要保证微服务内部业务的完整性为前提。目标是当我们要修改某个业务时,最好就只修改当前微服务,这样变更的成本更低。

一旦微服务做到了高内聚,那么服务之间的耦合度自然就降低了。

当然,微服务之间不可避免的会有或多或少的业务交互,比如下单时需要查询商品数据。这个时候我们不能在订单服务直接查询商品数据库,否则就导致了数据耦合。而应该由商品服务对应暴露接口,并且一定要保证微服务对外接口的稳定性(即:尽量保证接口外观不变)。虽然出现了服务间调用,但此时无论你如何在商品服务做内部修改,都不会影响到订单微服务,服务间的耦合度就降低了。


明确了拆分目标,接下来就是拆分方式了。我们在做服务拆分时一般有两种方式:

  • 纵向拆分

    所谓纵向拆分,就是按照项目的功能模块来拆分。例如示例商城中,就有用户管理功能、订单管理功能、购物车功能、商品管理功能、支付功能等。那么按照功能模块将他们拆分为一个个服务,就属于纵向拆分。这种拆分模式可以尽可能提高服务的内聚性。

  • 横向拆分

    横向拆分,是看各个功能模块之间有没有公共的业务部分,如果有将其抽取出来作为通用服务。例如用户登录是需要发送消息通知,记录风控数据,下单时也要发送短信,记录风控数据。因此消息发送、风控数据记录就是通用的业务功能,因此可以将他们分别抽取为公共服务:消息中心服务、风控管理服务。这样可以提高业务的复用性,避免重复开发。同时通用业务一般接口稳定性较强,也不会使服务之间过分耦合。

当然,由于示例商城并不是一个完整的项目,其中的短信发送、风控管理并没有实现,这里就不再考虑了。而其它的业务按照纵向拆分,可以分为以下几个微服务

  • 用户服务
  • 商品服务
  • 订单服务
  • 购物车服务
  • 支付服务

总结

  • 按照业务模块来拆分
  • 抽取公共服务,提高复用性

2.3 工程结构

工程结构有两种:

独立 Project:每一个服务是一个 Project,用一个文件夹存放这些服务。\

结构示例

├── user-service/          # 独立工程(Git仓库)
│   ├── src/               # 代码目录
│   └── pom.xml            # 独立构建文件
├── order-service/         # 独立工程
│   ├── src/
│   └── pom.xml
└── gateway/               # 独立工程
    ├── src/
    └── pom.xml

优点:

  • 完全解耦:每个服务独立仓库,物理隔离,避免误改其他服务代码。
  • 独立部署:CI/CD流水线可单独构建、测试、发布(适合大型团队分工)。
  • 权限控制:可针对不同仓库设置不同的开发者权限。

缺点:

  • 依赖管理困难:公共依赖(如工具类、Feign接口)需手动同步版本。
  • 开发环境复杂:需克隆多个仓库,本地启动多个服务。
  • 跨服务调试麻烦:需手动切换工程。

适用场景:

  • 团队规模大,服务由不同小组维护。
  • 服务生命周期差异大(如某些服务频繁迭代,其他服务稳定)。
  • 需要严格的代码权限隔离(如金融、政务系统)。

Maven 聚合:整个微服务项目是一个 Project,下面每一个服务是一个 model

结构示例

spring-cloud-project/       # 父工程(Git仓库)
├── pom.xml                # 聚合所有子模块
├── common/                # 公共模块
│   └── pom.xml
├── user-service/          # 子模块
│   ├── src/
│   └── pom.xml
├── order-service/         # 子模块
│   ├── src/
│   └── pom.xml
└── gateway/               # 子模块
    ├── src/
    └── pom.xml

优点:

  • 统一依赖管理:父POM统一定义版本,避免冲突。
  • 代码共享便捷:公共代码(如DTO、Utils)放在 common 模块,自动同步。
  • 开发调试高效:一键启动所有依赖服务(配合docker-compose)。
  • 重构安全:IDE可全局分析跨服务调用(如Feign接口)。

缺点:

  • 耦合风险:开发者可能误改其他模块代码。
  • 构建耗时:全量构建时需编译所有模块(可通过mvn -pl指定模块解决)。

适用场景:

  • 中小团队,服务关联紧密。
  • 快速迭代的全栈项目(如创业公司MVP)。
  • 需要频繁跨服务联调的场景。

2.4 Maven聚合方式服务拆分

  1. 在项目中创建module
  2. 选择maven模块,并设定JDK版本:
  3. 商品模块,我们起名为item-service
  4. 引入依赖
    • 即这个项目启动所需要的依赖pom文件
  5. 编写启动类
    • 即这个服务单独的Application启动类
  6. 编写是配置文件
    • 可以从原单体项目中拷贝然后修改
  7. 然后拷贝原单体项目中拷贝中与商品管理有关的代码到item-service
  8. 将所需要的数据库表新建一个MySQL实例进行存放
  9. 启动测试

总结:就是将要拆分的模块的代码,配置文件,启动类,数据库数据全部都拆到一个单独的新模块里面即可。

2.5 服务调用

服务拆分之后,一个模块需要使用到另一个模块的数据,可以向前端那样,基于 http 请求,去获取另一个模块的数据。

常见技术

  • HTTP/REST:通过 RestTemplateOpenFeign(Spring Cloud)或 Apache HttpClient
  • RPC:如 Dubbo、gRPC、Thrift。

适用场景:需要即时响应的操作(如查询订单详情)。

  • 优缺点
    • ✅ 简单直观,符合人类思维。
    • ❌ 调用链过长会导致延迟叠加(需优化或改用异步)。
对比项RPC 框架HTTP/REST
抽象层级方法调用(像调用本地函数一样)资源操作(基于 URI 和 HTTP 动词)
目标隐藏网络细节,专注业务逻辑标准化、无状态的资源交互
典型代表Dubbo、gRPC、ThriftSpring Cloud OpenFeign、RestTemplate

RestTemplate 示例

private void handleCartItems(List<CartVO> vos) {
    // TODO 1.获取商品id
    Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
    
    // 2.查询商品
    // List<ItemDTO> items = itemService.queryItemByIds(itemIds);
    // 2.1.利用RestTemplate发起http请求,得到http的响应
    ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
            "http://localhost:8081/items?ids={ids}", //url
            HttpMethod.GET, //方法
            null, //请求体
            new ParameterizedTypeReference<List<ItemDTO>>() { //返回值类型
            },
            Map.of("ids", CollUtil.join(itemIds, ",")) //请求参数
    );
    // 2.2.解析响应
    if(!response.getStatusCode().is2xxSuccessful()){
        // 查询失败,直接结束
        return;
    }
    List<ItemDTO> items = response.getBody();
    if (CollUtils.isEmpty(items)) {
        return;
    }
    // 3.转为 id 到 item的map
    Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
    // 4.写入vo
    for (CartVO v : vos) {
        ItemDTO item = itemMap.get(v.getItemId());
        if (item == null) {
            continue;
        }
        v.setNewPrice(item.getPrice());
        v.setStatus(item.getStatus());
        v.setStock(item.getStock());
    }
}