前置

一、微服务与单体架构

1.1 单体架构

  1. 定义:一个项目所有功能模块都在一个工程中开发,项目的部署和开发比较简单

  2. 缺点:

    • 团队协作成本高
    • 系统的发布效率差:一个项目的任意一个模块的bug都会导致整个项目需要重新编译部署
    • 系统可用性差:各模块之间会互相影响,一个热点的功能会耗尽大部分的服务器资源,导致其他功能模块服务低可用

1.2 微服务

  1. 定义:将一个项目中的不同模块拆分出来,独立部署为多个服务。
  2. 特点:
    • 单一职责:一个微服务负责一部分业务功能,核心模块不依赖其他模块
    • 团队自治:每个微服务有自己的一整套开发、测试、运维等人员
    • 服务自治:每个服务独立打包、部署,访问自己独立的数据库。

1.3 SpringCloud

SpringCloud是一个标准规范,其并不提供具体的服务,而SpringCloudAlibaba是一个满足SpringCloud规范的微服务框架的具体实现,它提供了具体的解决方案。两者关系类似于接口和实现类

使用方法
  在父工程里面引入SpringCloud和SpringCloudAlibaba的依赖管理,这样有关这两个依赖的版本就无需配置了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<properties>
<spring-cloud.version>2021.0.3</spring-cloud.version>
<spring-cloud-alibaba.version>2021.0.4.0</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<!--spring cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud alibaba-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

二、服务拆分和调用

  当一个项目是微服务架构时,难免会出现一个服务调用另一个服务功能的情况,那么此时就需要跨微服务的远程调用(RPC)

  此时由于微服务不在同一个模块中,并且每个微服务独立访问自己的数据库,服务之间不能直接通过对象调用,那么就需要http请求来调用服务接口,Spring提供了一个RestTemplate的API可以方便的发送http请求。

2.1 RestTemplate

使用方法

  1. 步骤一:创建一个config类,将RestTemplate注册为一个bean
    1
    2
    3
    4
    5
    6
    7
    8
    @Configuration
    public class RemoteCallConfig {

    @Bean
    public RestTemplate restTemplate() {
    return new RestTemplate();
    }
    }
  2. 步骤二:使用RestTemplate提供的exchange方法向其他服务发送http请求
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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, ","))
    );
    // 解析响应
    if(!response.getStatusCode().is2xxSuccessful()){
    // 查询失败,直接结束
    return;
    }
    List<ItemDTO> items = response.getBody();
    if (CollUtils.isEmpty(items)) {
    return;
    }

三、服务注册和发现

  上一小节使用restTemplate实现了对其他服务的远程调用,但是在代码中将发送http请求的IP和端口写“死”了,这样后面对服务进行多机部署时,只能访问其中一台服务器,没有进行负载均衡。

3.1 Nacos基本情况

  为了解决上述问题,需要一个能够管理所有所有服务的中间件,而Nacos就是这样一个中间件,它可以管理所有对外提供服务接口的服务器地址,并将服务推送到需要的服务中。

基本情况

  • 服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
  • 调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
  • 调用者自己对实例列表负载均衡,挑选一个实例
  • 调用者向该实例发起远程调用

当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?

  • 服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)
  • 当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除
  • 当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
  • 当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表

3.2 Nacos部署

  这里是用的是docker容器部署,Nacos部署需要一个使用到mysql数据库,同时有一个nacos/custom.env文件,里面包含mysql的地址信息,下面是mysql的表信息内容以及配置文件内容 (注:Nacos要在mysql之后启动,否则会连接不上nacos)

1
2
3
4
5
6
7
8
9
PREFER_HOST_MODE=hostname
MODE=standalone
SPRING_DATASOURCE_PLATFORM=mysql
MYSQL_SERVICE_HOST=192.168.223.101
MYSQL_SERVICE_DB_NAME=nacos
MYSQL_SERVICE_PORT=3306
MYSQL_SERVICE_USER=root
MYSQL_SERVICE_PASSWORD=123
MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai

  随后使用docker进行部署

1
2
3
4
5
6
7
8
docker run -d \
--name nacos \
--env-file ./nacos/custom.env \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
--restart=always \
nacos/nacos-server:v2.1.0-slim

3.3 服务注册

  nacos部署完成后,可以通过8848端口访问测试,若成功部署,接下来就是在java中使用nacos提供的服务。
  首先是服务注册,若一个服务需要对外提供服务接口,那么就需要在Nacos中注册,分为以下两步:

  1. 步骤一:引入Nacos依赖
    1
    2
    3
    4
    5
    <!--nacos 服务注册发现-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
  2. 步骤二:配置Nacos
    1
    2
    3
    4
    5
    6
    spring:
    application:
    name: item-service # 服务名称
    cloud:
    nacos:
    server-addr: 192.168.150.101:8848 # nacos地址

3.4 服务发现

  服务的消费者需要订阅服务,此时有以下三步:

  1. 步骤一:引入Nacos依,与服务注册依赖一致
    1
    2
    3
    4
    5
    <!--nacos 服务注册发现-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
  2. 步骤二:配置Nacos地址,服务发现不依赖服务名称,但是一般来说也会配置服务名称
    1
    2
    3
    4
    spring:
    cloud:
    nacos:
    server-addr: 192.168.150.101:8848
  3. 步骤三:使用SpringCloud提供的DiscoveryClient发现服务,随后进行负载均衡,再调用restTemplate进行发起http请求。这里与直接使用restTemplate不同的地方是,首先发现所有的服务实例,随后获取一个实例,最后通过实例获取服务的ip地址和端口号
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 2.1 发现服务的所有实例
    List<ServiceInstance> instances = discoveryClient.getInstances("item-service");

    // 2.2 使用负载均衡算法选择一个实例
    ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));

    // 2.3 向商品管理微服务发起http请求
    ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
    instance.getUri() + "/items?ids={ids}",
    HttpMethod.GET,
    null,
    new ParameterizedTypeReference<List<ItemDTO>>() {},
    Map.of("ids", CollUtil.join(itemIds, ","))
    );

四、OpenFeign

  上一小节使用了Nacos实现了服务治理,也就是其中管理所有提供服务接口的服务地址,但是使用起来比较复杂,而OpenFeign组件可以大大简化RPC的过程。

4.1 快速入门

  1. 步骤一:创建一个新模块hm-api,专门用来提供所有服务接口的。创建这个模块的目的是可能多个模块都需要调用同一个服务的接口,这样可以提供一个统一的服务接口,不用每个模块都创建服务接口

  2. 步骤二:需要引入两个依赖,一个是OpenFeign,另一个是负载均衡依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!--openFeign-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--负载均衡器-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
  3. 步骤三:在使用服务接口的启动类上添加注解,启用OpenFeign功能,其中basePackages的值是统一服务接口的包名

    1
    @EnableFeignClients(basePackages = "com.hmall.api.client")
  4. 步骤四:编写OpenFeign客户端,在hm-api模块中新建client包,创建如下接口(示例)
    该接口指明了服务提供者的服务名称
    对应的方法上有请求路径和参数以及返回值

    1
    2
    3
    4
    5
    6
    @FeignClient("item-service")
    public interface ItemClient {

    @GetMapping("/items")
    List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
    }
  5. 步骤五:服务消费者引入hm-api依赖

    1
    2
    3
    4
    5
    6
    <!--feign模块-->
    <dependency>
    <groupId>com.heima</groupId>
    <artifactId>hm-api</artifactId>
    <version>1.0.0</version>
    </dependency>
  6. 步骤六:服务消费者创建ItemClient对象,并通过该对象调用在hm-api中声明的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Service
    @RequiredArgsConstructor
    public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {
    private final ItemClient itemClient;
    private void handleCartItems(List<CartVO> vos) {
    List<ItemDTO> items = itemClient.queryItemsByIds(itemIds);

    Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));

    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());
    }
    }
    }

4.2 配置连接池

  OpenFeign发起http请求时使用的http客户端基于HttpURLConnection,该http客户端不支持连接池,效率较低,因此配置为支持连接池的OKHttp

  1. 步骤一:在服务消费者中引入OKHttp客户端的依赖

    1
    2
    3
    4
    5
    <!--OK http 的依赖 -->
    <dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
    </dependency>
  2. 步骤二:配置连接池生效

    1
    2
    3
    feign:
    okhttp:
    enabled: true # 开启OKHttp功能