【Spring Cloud 微服务】——第一章 微服务入门与服务拆分

目录

前言

[1. 认识微服务](#1. 认识微服务)

[1.1. 单体架构](#1.1. 单体架构)

[1.2. 微服务](#1.2. 微服务)

[1.3. Spring Cloud](#1.3. Spring Cloud)

[2. 微服务拆分](#2. 微服务拆分)

[2.1. 服务拆分原则](#2.1. 服务拆分原则)

[2.1.1. 什么时候拆](#2.1.1. 什么时候拆)

[2.1.2. 怎么拆](#2.1.2. 怎么拆)

[2.2. 拆分购物车、商品服务](#2.2. 拆分购物车、商品服务)

[2.2.1. 创建商品服务](#2.2.1. 创建商品服务)

[2.2.2. 创建购物车服务](#2.2.2. 创建购物车服务)

[2.3. 服务调用](#2.3. 服务调用)

[2.3.1. RestTemplate](#2.3.1. RestTemplate)

[2.3.2. 远程调用](#2.3.2. 远程调用)

[2.4. 总结](#2.4. 总结)


本文介绍了微服务架构的核心概念、拆分原则及实践方法。首先对比了单体架构与微服务架构的优缺点,指出微服务通过拆分功能模块为独立服务,解决了团队协作、发布效率和系统可用性问题。文章详细讲解了微服务的拆分时机(初创项目先单体后拆分,大型项目直接采用)和拆分原则(高内聚、低耦合),并以电商系统为例演示了商品和购物车服务的拆分过程。最后通过RestTemplate实现服务间HTTP调用,展示了微服务间通信的基本方式。文章还预告了后续将介绍服务注册发现和OpenFeign远程调用技术。

前言

在互联网时代,越来越多的一线互联网公司都在使用微服务技术。

那么:

  • 到底什么是微服务?

  • 企业该不该引入微服务?

  • 微服务技术该如何在企业落地?

本篇文章带你揭开微服务的神秘面纱。


1. 认识微服务

1.1. 单体架构

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

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

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

  • 团队协作成本高:所有模块都在一个项目中,不同模块的代码之间物理边界越来越模糊。最终要把功能合并到一个分支,开发人员会陷入到解决冲突的泥潭之中。

  • 系统发布效率低:任何模块变更都需要发布整个系统,往往一次发布需要数十分钟甚至数小时。

  • 系统可用性差:单体架构各个功能模块相互之间会互相影响,热点功能会耗尽系统资源,导致其它服务低可用。

例如,当系统中某个接口是并发较高的热点接口时,该接口耗尽服务端资源,最终会导致其它本来正常的接口被拖慢,甚至因超时而无法访问。

可见,单体架构的扩展性较差,功能之间相互影响比较大。

1.2. 微服务

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

  • 单一职责:一个微服务负责一部分业务功能,并且其核心数据不依赖于其它模块。

  • 团队自治:每个微服务都有自己独立的开发、测试、发布、运维人员,团队人员规模不超过10人。

  • 服务自治:每个微服务都独立打包部署,访问自己独立的数据库。并且要做好服务隔离,避免对其它微服务产生影响。

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

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

  • 团队协作成本高?

    • 由于服务拆分,每个服务代码量大大减少,协作成本大大降低
  • 系统发布效率低?

    • 每个服务都是独立部署,当有某个服务有代码变更时,只需要打包部署该服务即可
  • 系统可用性差?

    • 每个服务独立部署,并且做好服务隔离,使用自己的服务器资源,不会影响到其它服务。

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

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

  • 如果出现跨服务的业务该如何处理?

  • 页面请求到底该访问哪个服务?

  • 如何实现各个服务之间的服务隔离?

这些问题,我们在后续的学习中会给大家逐一解答。

1.3. Spring Cloud

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

而且 Spring Cloud 依托于 Spring Boot 的自动装配能力,大大降低了其项目搭建、组件使用的成本。

官网地址:Spring Cloud

Spring Cloud 版本对应关系

Spring Cloud 版本 Spring Boot 版本
2022.0.x (Kilburn) 3.0.x
2021.0.x (Jubilee) 2.6.x, 2.7.x
2020.0.x (Ilford) 2.4.x, 2.5.x
Hoxton 2.2.x, 2.3.x
Greenwich 2.1.x
Finchley 2.0.x

推荐使用次新版本:Spring Cloud 2021.0.x 以及 Spring Boot 2.7.x 版本。

另外,Alibaba 的微服务产品 Spring Cloud Alibaba 目前也成为了 Spring Cloud 组件中的一员。


2. 微服务拆分

2.1. 服务拆分原则

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

  • 什么时候拆?

  • 如何拆?

2.1.1. 什么时候拆

一般情况下,对于一个初创的项目,首先要做的是验证项目的可行性。因此这一阶段的首要任务是敏捷开发,快速产出生产可用的产品,投入市场做验证。

所以,对于大多数小型项目来说,一般是先采用单体架构,随着用户规模扩大、业务复杂后再逐渐拆分为微服务架构。这样初期成本会比较低,可以快速试错。

而对于一些大型项目,在立项之初目的就很明确,为了长远考虑,在架构设计时就直接选择微服务架构。

2.1.2. 怎么拆

微服务拆分时粒度要小,具体可以从两个角度来分析:

  • 高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高。

  • 低耦合:每个微服务的功能要相对独立,尽量减少对其它微服务的依赖,或者依赖接口的稳定性要强。

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

  • 纵向拆分:按照项目的功能模块来拆分。例如用户管理功能、订单管理功能、购物车功能、商品管理功能、支付功能等。这种拆分模式可以尽可能提高服务的内聚性。

  • 横向拆分:看各个功能模块之间有没有公共的业务部分,如果有将其抽取出来作为通用服务。例如消息发送、风控数据记录就是通用的业务功能,可以抽取为公共服务。

按照纵向拆分,可以分为以下几个微服务:

  • 用户服务

  • 商品服务

  • 订单服务

  • 购物车服务

  • 支付服务

2.2. 拆分购物车、商品服务

接下来,我们先把商品管理功能、购物车功能抽取为两个独立服务。

一般微服务项目有两种不同的工程结构:

  • 完全解耦:每一个微服务都创建为一个独立的工程,甚至可以使用不同的开发语言来开发,项目完全解耦。

    • 优点:服务之间耦合度低

    • 缺点:每个项目都有自己的独立仓库,管理起来比较麻烦

  • Maven 聚合:整个项目为一个 Project,然后每个微服务是其中的一个 Module

    • 优点:项目代码集中,管理和运维方便

    • 缺点:服务之间耦合,编译时间较长

2.2.1. 创建商品服务

创建 Maven 模块 item-service后

引入依赖:

java 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>demo-parent</artifactId>
        <groupId>com.example</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
​
    <artifactId>item-service</artifactId>
​
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>
​
    <dependencies>
        <!--common-->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>common</artifactId>
            <version>1.0.0</version>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--数据库-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <!--单元测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
​
    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

启动类:

java 复制代码
package com.example.item;
​
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
​
@MapperScan("com.example.item.mapper")
@SpringBootApplication
public class ItemApplication {
    public static void main(String[] args) {
        SpringApplication.run(ItemApplication.class, args);
    }
}

配置文件 application.yaml

java 复制代码
server:
  port: 8081
spring:
  application:
    name: item-service
  profiles:
    active: dev
  datasource:
    url: jdbc:mysql://${db.host}:3306/hm-item?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: ${db.pw}
mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
  global-config:
    db-config:
      update-strategy: not_null
      id-type: auto
logging:
  level:
    com.example: debug
  pattern:
    dateformat: HH:mm:ss:SSS
  file:
    path: "logs/${spring.application.name}"
knife4j:
  enable: true
  openapi:
    title: 商品服务接口文档
    description: "商品服务接口文档"
    email: dev@example.com
    concat: 开发者
    url: https://www.example.com
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:
          - com.example.item.controller
2.2.2. 创建购物车服务

创建 Maven 模块 cart-service:

启动类:

java 复制代码
package com.example.cart;
​
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
​
@MapperScan("com.example.cart.mapper")
@SpringBootApplication
public class CartApplication {
    public static void main(String[] args) {
        SpringApplication.run(CartApplication.class, args);
    }
}

配置文件 application.yaml

java 复制代码
server:
  port: 8082
spring:
  application:
    name: cart-service
  profiles:
    active: dev
  datasource:
    url: jdbc:mysql://${db.host}:3306/hm-cart?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: ${db.pw}
mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
  global-config:
    db-config:
      update-strategy: not_null
      id-type: auto
logging:
  level:
    com.example: debug
  pattern:
    dateformat: HH:mm:ss:SSS
  file:
    path: "logs/${spring.application.name}"
knife4j:
  enable: true
  openapi:
    title: 购物车服务接口文档
    description: "购物车服务接口文档"
    email: dev@example.com
    concat: 开发者
    url: https://www.example.com
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:
          - com.example.cart.controller

2.3. 服务调用

在拆分的时候,我们发现一个问题:购物车业务中需要查询商品信息,但商品信息查询的逻辑全部迁移到了 item-service 服务,导致无法查询。

因此,现在查询购物车列表的流程变成了这样:

那么问题来了:我们该如何在 cart-service 中实现对 item-service 服务的查询呢?

答案是通过 HTTP 请求的方式来完成的,不仅仅可以实现远程查询,还可以实现新增、删除等各种远程请求。

2.3.1. RestTemplate

Spring 给我们提供了一个 RestTemplate 的 API,可以方便的实现 Http 请求的发送。

注册 RestTemplate:

java 复制代码
package com.example.cart.config;
​
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
​
@Configuration
public class RemoteCallConfig {
​
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
2.3.2. 远程调用

修改 CartServiceImpl 的 handleCartItems 方法:

java 复制代码
private void handleCartItems(List<CartVO> vos) {
    // 1.获取商品id
    Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
    // 2.查询商品
    // 2.1.利用RestTemplate发起http请求
    ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
            "http://localhost:8081/items?ids={ids}",
            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());
    }
}

其中 item-service 提供查询接口,cart-service 利用 Http 请求调用该接口。因此 item-service 可以称为服务的提供者,而 cart-service 则称为服务的消费者或服务调用者。

2.4. 总结

什么时候需要拆分微服务?

  • 如果是创业型公司,最好先用单体架构快速迭代开发,验证市场运作模型,快速试错。当业务规模扩大、人员规模增加后,再考虑拆分微服务。

  • 如果是大型企业,有充足的资源,可以在项目开始之初就搭建微服务架构。

如何拆分?

  • 首先要做到高内聚、低耦合

  • 从拆分方式来说,有横向拆分和纵向拆分两种。纵向就是按照业务功能模块,横向则是拆分通用性业务,提高复用性

服务拆分之后,不可避免的会出现跨微服务的业务,此时微服务之间就需要进行远程调用。微服务之间的远程调用被称为 RPC(Remote Procedure Call)。

Java 发送 http 请求可以使用 Spring 提供的 RestTemplate:

  • 注册 RestTemplate 到 Spring 容器

  • 调用 RestTemplate 的 API 发送请求

    • getForObject:发送 Get 请求并返回指定类型对象

    • PostForObject:发送 Post 请求并返回指定类型对象

    • put:发送 PUT 请求

    • delete:发送 Delete 请求

    • exchange:发送任意类型请求,返回 ResponseEntity


下一篇将介绍:服务注册与发现(Nacos)和远程调用(OpenFeign)