自学内容网 自学内容网

Day1 微服务 单体架构、微服务架构、微服务拆分、服务远程调用、服务注册和发现Nacos、OpenFeign

目录

1.导入单体架构项目

1.1 安装mysql

1.2 后端

1.3 前端

2.微服务

2.1 单体架构

2.2 微服务

2.3 SpringCloud

3.微服务拆分

3.1 服务拆分原则

3.1.1 什么时候拆

3.1.2 怎么拆

3.2 拆分购物车、商品服务

3.2.1 商品服务

3.2.2 购物车服务

3.3 服务调用

3.3.1 RestTemplate

3.3.2 远程调用

4.服务注册和发现

4.1 注册中心原理

4.2 Nacos注册中心

4.3 服务注册

4.3.1 添加依赖

4.3.2 配置Nacos

4.3.3 启动服务实例

4.4.服务发现

4.4.1 引入依赖

4.4.2 配置Nacos

4.4.3 发现并调用服务

5.OpenFeign

5.1 快速入门

5.1.1 引入依赖

5.1.2 启用OpenFeign

5.1.3 编写OpenFeign客户端

5.1.4 使用FeignClient

5.2 连接池

5.2.1 引入依赖

5.2.2 开启连接池

5.2.3 验证

5.3 最佳实践

5.3.1 思路分析

5.3.2 抽取Feign客户端

5.3.3 扫描包

5.4 日志配置

5.4.1 定义日志级别

5.4.2 配置日志级别


今天学习的思维导图:

1.导入单体架构项目

1.1 安装mysql

资料提供好了MySQL的一个目录:

其中有MySQL的配置文件和初始化脚本:

复制到虚拟机的/root目录。如果/root下已经存在mysql目录则删除旧的,如果不存在则直接复制本地的:

然后创建一个通用网络:

docker network create hm-net

使用下面的命令来安装MySQL:

docker run -d \
  --name mysql \
  -p 3306:3306 \
  -e TZ=Asia/Shanghai \
  -e MYSQL_ROOT_PASSWORD=123 \
  -v /root/mysql/data:/var/lib/mysql \
  -v /root/mysql/conf:/etc/mysql/conf.d \
  -v /root/mysql/init:/docker-entrypoint-initdb.d \
  --network hm-net\
  mysql

创建以及运行mysql容器,此时因为存在挂载会执行配置文件,以及初始化的SQL文件。

查看容器状态:

docker ps

此时,如果我们使用MySQL的客户端工具连接MySQL,应该能发现已经创建了黑马商城所需要的表:

1.2 后端

然后是Java代码,在资料提供了一个hmall目录:

将其复制到你的工作空间,然后利用Idea打开。

项目结构如下:

按下ALT + 8键打开services窗口,新增一个启动项:

找到Spring Boot,开启服务:

点击后应该会在services中出现hmall的启动项:

点击对应按钮,即可实现运行或DEBUG运行。

不过别着急!!

我们还需要对这个启动项做简单配置,在HMallApplication上点击鼠标右键,会弹出窗口,然后选择Edit Configuration

在弹出窗口中配置SpringBoot的启动环境为local:

local:因为是在本地运行的项目,local配置了Linux的地址。

dev:在Linux运行,可以直接配置容器名,会被解析成对应的地址。

点击OK配置完成。接下来就可以运行了!

启动完成后,试试看访问下http:// http://localhost:8080/hi吧!

1.3 前端

在资料中还提供了一个hmall-nginx的目录:

其中就是一个nginx程序以及我们的前端代码,直接在windows下将其复制到一个非中文、不包含特殊字符的目录下。

# 启动nginx
start nginx.exe
# 停止
nginx.exe -s stop
# 重新加载配置
nginx.exe -s reload
# 重启
nginx.exe -s restart

特别注意:

nginx.exe 不要双击启动,而是打开cmd窗口,通过命令行启动。停止的时候也一样要是用命令停止。如果启动失败不要重复启动,而是查看logs目录中的error.log日志,查看是否是端口冲突。如果是端口冲突则自行修改端口解决。

启动成功后,访问http://localhost:18080,应该能看到我们的门户页面:

2.微服务

2.1 单体架构

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

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

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

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

  • 系统发布效率低:任何模块变更都需要发布整个系统,而系统发布过程中需要多个模块之间制约较多,需要对比各种文件,任何一处出现问题都会导致发布失败,往往一次发布需要数十分钟甚至数小时。

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

2.2 微服务

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

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

  • 团队自治:每个微服务都有自己独立的开发、测试、发布、运维人员,团队人员规模不超过10人(2张披萨能喂饱)

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

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

比如:

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

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

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

2.3 SpringCloud

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

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

版本之间需要对应:

在我们的父工程hmall中已经配置了SpringCloud以及SpringCloudAlibaba的依赖:

<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>

对应的版本:

这样,我们在后续需要使用SpringCloud或者SpringCloudAlibaba组件时,就无需单独指定版本了。

3.微服务拆分

接下来,黑马商城这个单体项目拆分为微服务项目。

黑马商城项目的基本结构:

3.1 服务拆分原则

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

  • 什么时候拆?

  • 如何拆?

3.1.1 什么时候拆

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

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

3.1.2 怎么拆

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

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

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

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

  • 纵向拆分:就是按照项目的功能模块来拆分。

  • 横向拆分:看各个功能模块之间有没有公共的业务部分,如果有将其抽取出来作为通用服务。

3.2 拆分功能

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

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

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

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

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

    • 优点:项目代码集中,管理和运维方便(授课也方便)

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

3.2.1 商品服务

在hmall中创建module:

商品模块,我们起名为item-service

引入依赖:

<?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>hmall</artifactId>
        <groupId>com.heima</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.heima</groupId>
            <artifactId>hm-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>

编写启动类:

编写配置文件:

server:
  port: 8081
spring:
  application:
    name: item-service
  profiles:
    active: dev
  datasource:
    url: jdbc:mysql://${hm.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: ${hm.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.hmall: debug
  pattern:
    dateformat: HH:mm:ss:SSS
  file:
    path: "logs/${spring.application.name}"
knife4j:
  enable: true
  openapi:
    title: 商品服务接口文档
    description: "信息"
    email: zhanghuyi@itcast.cn
    concat: 虎哥
    url: https://www.itcast.cn
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:
          - com.hmall.item.controller

然后拷贝hm-service中与商品管理有关的代码到item-service,如图:

这里有一个地方的代码需要改动,就是ItemServiceImpl中的deductStock方法:

改动前:

改动后:

导入数据库:


可以启动测试了,在启动前我们要配置一下启动项,默认激活的配置为local而不是dev

在打开的编辑框填写active profiles:

接着,启动item-service,访问商品微服务的swagger接口文档:http://localhost:8081/doc.html

3.2.2 购物车服务

与商品服务类似,在hmall下创建一个新的module,起名为cart-service:

然后是依赖:

<?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>hmall</artifactId>
        <groupId>com.heima</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cart-service</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <!--common-->
        <dependency>
            <groupId>com.heima</groupId>
            <artifactId>hm-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>

然后是启动类:

@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
    public static void main(String[] args) {
        SpringApplication.run(CartApplication.class, args);
    }
}

配置文件,同样可以拷贝自item-service,不过其中的application.yaml需要修改:

server:
  port: 8082
spring:
  application:
    name: cart-service
  profiles:
    active: dev
  datasource:
    url: jdbc:mysql://${hm.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: ${hm.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.hmall: debug
  pattern:
    dateformat: HH:mm:ss:SSS
  file:
    path: "logs/${spring.application.name}"
knife4j:
  enable: true
  openapi:
    title: 商品服务接口文档
    description: "信息"
    email: zhanghuyi@itcast.cn
    concat: 虎哥
    url: https://www.itcast.cn
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:
          - com.hmall.cart.controller

把hm-service中的与购物车有关功能拷贝过来,最终的项目结构如下:

特别注意的是com.hmall.cart.service.impl.CartServiceImpl其中有两个地方需要处理:

  • 需要获取登录用户信息,但登录校验功能目前没有复制过来,先写死固定用户id

  • 查询购物车时需要查询商品信息而商品信息不在当前服务,不能完成对象注入

导入数据库表:

就可以测试了。不过在启动前,同样要配置启动项的active profilelocal

然后启动CartApplication,访问swagger文档页面:http://localhost:8082/doc.html

测试其中的查询我的购物车列表接口,无需填写参数,直接访问:

注意事项:其中与商品有关的几个字段值都为空!

那么,我们该如何在cart-service服务中实现对item-service服务的查询呢?

答案:通过远程服务调用。

3.2.3 交易服务(实战)

3.2.3.1 创建项目

在hmall下新建一个module,命名为trade-service:

3.2.3.2 依赖

trade-service的pom.xml文件内容如下:

<?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>hmall</artifactId>
        <groupId>com.heima</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>trade-service</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <!--common-->
        <dependency>
            <groupId>com.heima</groupId>
            <artifactId>hm-common</artifactId>
            <version>1.0.0</version>
        </dependency>
        <!--api-->
        <dependency>
            <groupId>com.heima</groupId>
            <artifactId>hm-api</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>
        <!--nacos 服务注册发现-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</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>

3.2.3.3 启动类

在trade-service中的com.hmall.trade包下创建启动类:

@EnableFeignClients(basePackages = "com.hmall.api.client", defaultConfiguration = DefaultFeignConfig.class)
@MapperScan("com.hmall.trade.mapper")
@SpringBootApplication
public class TradeApplication {
    public static void main(String[] args) {
        SpringApplication.run(TradeApplication.class, args);
    }
}

3.2.3.4 配置文件

hm-service项目中复制3个yaml配置文件到trade-serviceresource目录。

其中application-dev.yamlapplication-local.yaml保持不变。application.yaml如下:

server:
  port: 8085
spring:
  application:
    name: trade-service # 服务名称
  profiles:
    active: dev
  datasource:
    url: jdbc:mysql://${hm.db.host}:3306/hm-trade?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: ${hm.db.pw}
  cloud:
    nacos:
      server-addr: 192.168.150.101 # nacos地址
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.hmall: debug
  pattern:
    dateformat: HH:mm:ss:SSS
  file:
    path: "logs/${spring.application.name}"
knife4j:
  enable: true
  openapi:
    title: 交易服务接口文档
    description: "信息"
    email: zhanghuyi@itcast.cn
    concat: 虎哥
    url: https://www.itcast.cn
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:
          - com.hmall.trade.controller

3.2.3.5 代码

复制hm-service中所有与trade有关的代码,最终项目结构如下:

在交易服务中,用户下单时需要做下列事情:

  • 根据id查询商品列表

  • 计算商品总价

  • 保存订单

  • 扣减库存

  • 清理购物车商品

其中,查询商品、扣减库存都是与商品有关的业务,在item-service中有相关功能;清理购物车商品是购物车业务,在cart-service中有相关功能。

因此交易服务要调用他们,通过OpenFeign远程调用。我们需要将上述功能抽取为FeignClient。

抽取ItemClient接口:

首先是扣减库存,在item-service中的对应业务接口如下:

将这个接口抽取到hm-api模块的com.hmall.api.client.ItemClient中:

将接口参数的OrderDetailDTO抽取到hm-api模块的com.hmall.api.dto包下:

抽取CartClient接口:

接下来是清理购物车商品,在cart-service中的对应业务接口如下:

我们在hm-api模块的com.hmall.api.client包下定义一个CartClient接口:

代码如下:



@FeignClient("cart-service")
public interface CartClient {
    @DeleteMapping("/carts")
    void deleteCartItemByIds(@RequestParam("ids") Collection<Long> ids);
}

改造OrderServiceImpl:


/**
 * <p>
 * 服务实现类
 * </p>
 */
@Service
@RequiredArgsConstructor
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {

    private final ItemClient itemClient;
    private final IOrderDetailService detailService;
    private final CartClient cartClient;

    @Override
    @Transactional
    public Long createOrder(OrderFormDTO orderFormDTO) {
        // 1.订单数据
        Order order = new Order();
        // 1.1.查询商品
        List<OrderDetailDTO> detailDTOS = orderFormDTO.getDetails();
        // 1.2.获取商品id和数量的Map
        Map<Long, Integer> itemNumMap = detailDTOS.stream()
                .collect(Collectors.toMap(OrderDetailDTO::getItemId, OrderDetailDTO::getNum));
        Set<Long> itemIds = itemNumMap.keySet();
        // 1.3.查询商品
        List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
        if (items == null || items.size() < itemIds.size()) {
            throw new BadRequestException("商品不存在");
        }
        // 1.4.基于商品价格、购买数量计算商品总价:totalFee
        int total = 0;
        for (ItemDTO item : items) {
            total += item.getPrice()  itemNumMap.get(item.getId());
        }
        order.setTotalFee(total);
        // 1.5.其它属性
        order.setPaymentType(orderFormDTO.getPaymentType());
        order.setUserId(UserContext.getUser());
        order.setStatus(1);
        // 1.6.将Order写入数据库order表中
        save(order);

        // 2.保存订单详情
        List<OrderDetail> details = buildDetails(order.getId(), items, itemNumMap);
        detailService.saveBatch(details);

        // 3.扣减库存
        try {
            itemClient.deductStock(detailDTOS);
        } catch (Exception e) {
            throw new RuntimeException("库存不足!");
        }

        // 4.清理购物车商品
        cartClient.deleteCartItemByIds(itemIds);
        return order.getId();
    }

    private List<OrderDetail> buildDetails(Long orderId, List<ItemDTO> items, Map<Long, Integer> numMap) {
        List<OrderDetail> details = new ArrayList<>(items.size());
        for (ItemDTO item : items) {
            OrderDetail detail = new OrderDetail();
            detail.setName(item.getName());
            detail.setSpec(item.getSpec());
            detail.setPrice(item.getPrice());
            detail.setNum(numMap.get(item.getId()));
            detail.setItemId(item.getId());
            detail.setImage(item.getImage());
            detail.setOrderId(orderId);
            details.add(detail);
        }
        return details;
    }
}

3.2.3.6 数据库

trade-service也需要自己的独立的database,向MySQL中导入课前资料提供的SQL:

3.2.3.7 配置启动项

给trade-service配置启动项,设置profile为local:

3.2.3.8 测试

启动TradeApplication,访问http://localhost:8085/doc.html,测试查询订单接口:

注意事项:创建订单接口无法测试,因为无法获取登录用户信息。

3.3 服务远程调用

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

最终结果就是查询到的购物车数据不完整,因此要想解决这个问题,我们就必须改造其中的代码,把原本本地方法调用,改造成跨微服务的远程调用

解决方案:Java代码发送Http的请求。

让购物车服务发送上述的HTTP请求,然后得到商品信息。

3.3.1 RestTemplate

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

其中提供了大量的方法,方便我们发送Http请求,例如:

可以看到常见的Get、Post、Put、Delete请求都支持,还可以使用exchange方法来构造复杂的请求。

cart-service服务中定义一个配置类:

先将RestTemplate注册为一个Bean:

@Configuration
public class RemoteCallConfig {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

3.3.2 远程调用

接下来,修改cart-service中的com.hmall.cart.service.impl.CartServiceImplhandleCartItems方法,发送http请求到item-service

可以看到,利用RestTemplate发送http请求与前端ajax发送请求非常相似,都包含四部分信息:

  • ① 请求方式

  • ② 请求路径

  • ③ 请求参数

  • ④ 返回值类型

handleCartItems方法的完整代码如下:

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

现在重启cart-service,再次测试查询我的购物车列表接口:

可以发现,所有商品相关数据都已经查询到了。

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

4.服务注册和发现

在上一章我们实现了微服务拆分,通过Http请求实现了跨微服务的远程调用。不过这种手动发送Http请求的方式存在一些问题。

试想一下,假如商品微服务被调用较多,为了应对更高的并发,进行了多实例部署,如图:

此时,每个item-service的实例其IP或端口不同,问题来了:

  • item-service这么多实例,cart-service如何知道每一个实例的地址?

  • http请求要写url地址,cart-service服务到底该调用哪个实例呢?

  • 如果在运行过程中,某一个item-service实例宕机,cart-service依然在调用该怎么办?

  • 如果并发太高,item-service临时多部署了N台实例,cart-service如何知道新实例的地址?

为了解决上述问题,就必须引入注册中心的概念,接下来我们就一起来分析下注册中心的原理。

4.1 注册中心原理

在微服务远程调用的过程中,包括两个角色:

  • 服务提供者:提供接口供其它微服务访问,比如item-service

  • 服务消费者:调用其它微服务提供的接口,比如cart-service

在大型微服务项目中,服务提供者的数量会非常多,为了管理这些服务就引入了注册中心的概念。

流程如下:

  • 服务启动时就会注册服务信息(服务名、IP、端口)到注册中心

  • 调用者从注册中心订阅服务,获取服务对应的实例列表(1个服务可能多实例部署)

  • 调用者自己对实例列表负载均衡,挑选一个实例

  • 调用者向该实例发起远程调用

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

  • 服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)

  • 当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除

  • 当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表

  • 当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表

4.2 Nacos注册中心

目前开源的注册中心框架有很多,国内比较常见的有:

  • Eureka:Netflix公司出品,目前被集成在SpringCloud当中,一般用于Java应用

  • Nacos:Alibaba公司出品,目前被集成在SpringCloudAlibaba中,一般用于Java应用

  • Consul:HashiCorp公司出品,目前集成在SpringCloud中,不限制微服务语言

基于Docker来部署Nacos的注册中心,首先我们要准备MySQL数据库表,用来存储Nacos的数据。由于是Docker部署,所以大家需要将资料中的SQL文件导入到你Docker中的MySQL容器中:

最终表结构如下:

找到课前资料下的nacos文件夹:

其中的nacos/custom.env文件中,有一个MYSQL_SERVICE_HOST也就是mysql地址,需要修改为你自己的虚拟机IP地址:

然后,将课前资料中的nacos目录上传至虚拟机的/root目录。

进入root目录,然后执行下面的docker命令:

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

启动完成后,访问下面地址:http://192.168.150.101:8848/nacos/注意将192.168.150.101替换为你自己的虚拟机IP地址。

首次访问会跳转到登录页,账号密码都是nacos

4.3 服务注册

接下来,我们把item-service注册到Nacos,步骤如下:

  • 引入依赖

  • 配置Nacos地址

  • 重启

4.3.1 添加依赖

item-servicepom.xml中添加依赖:

<!--nacos 服务注册发现-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

4.3.2 配置Nacos

item-serviceapplication.yml中添加nacos地址配置:

spring:
  application:
    name: item-service # 服务名称
  cloud:
    nacos:
      server-addr: 192.168.150.101:8848 # nacos地址

4.3.3 启动服务实例

为了测试一个服务多个实例的情况,我们再配置一个item-service的部署实例:

然后配置启动项,注意重命名并且配置新的端口,避免冲突:

重启item-service的两个实例:

访问nacos控制台,可以发现服务注册成功:

点击详情,可以查看到item-service服务的两个实例信息:

4.4.服务发现

服务的消费者要去nacos订阅服务,这个过程就是服务发现,步骤如下:

  • 引入依赖

  • 配置Nacos地址

  • 发现并调用服务

4.4.1 引入依赖

我们在cart-service中的pom.xml中添加下面的依赖:

<!--nacos 服务注册发现-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

可以发现,这里Nacos的依赖于服务注册时一致,这个依赖中同时包含了服务注册和发现的功能。因为任何一个微服务都可以调用别人,也可以被别人调用,即可以是调用者,也可以是提供者。

4.4.2 配置Nacos

cart-serviceapplication.yml中添加nacos地址配置:

spring:
  cloud:
    nacos:
      server-addr: 192.168.150.101:8848

4.4.3 发现并调用服务

服务调用者cart-service就可以去订阅item-service服务了。不过item-service有多个实例,而真正发起调用时只需要知道一个实例的地址。

常见的负载均衡算法

  • 随机

  • 轮询

  • IP的hash

  • 最近最少访问

另外,服务发现需要用到一个工具DiscoveryClient,SpringCloud已经帮我们自动装配,我们可以直接注入使用:

接下来,我们就可以对原来的远程调用做修改了,之前调用时我们需要写死服务提供者的IP和端口:

但现在不需要了,通过DiscoveryClient发现服务实例列表,然后通过负载均衡算法,选择一个实例去调用:

经过swagger测试,发现没有任何问题。

5.OpenFeign

可以实现远程调用像本地方法调用一样简单将其他服务的接口写入OpenFeign,并且指定服务名称,以及服务相关信息,会自定帮我们发HTTP请求,并且做负载均衡。从而实现跨服务访问。

OpenFeign替我们完成了服务拉取、负载均衡、发送http请求的所有工作,

在上一章,利用Nacos实现了服务的治理,利用RestTemplate实现了服务的远程调用。

但是远程调用的代码太复杂了:

而且这种调用方式,与原本的本地方法调用差异太大,编程时的体验也不统一,一会儿远程调用,一会儿本地调用。

因此,我们必须想办法改变远程调用的开发模式,让远程调用像本地方法调用一样简单而这就要用到OpenFeign组件了。

其实远程调用的关键点就在于四个:

  • 请求方式

  • 请求路径

  • 请求参数

  • 返回值类型

所以,OpenFeign就利用SpringMVC的相关注解来声明上述4个参数,然后基于动态代理帮我们生成远程调用的代码而无需我们手动再编写,非常方便。

5.1 快速入门

5.1.1 引入依赖

cart-service服务的pom.xml中引入OpenFeign的依赖和loadBalancer依赖:

  <!--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>

5.1.2 启用OpenFeign


接下来,我们在cart-service的CartApplication启动类上添加注解,启动OpenFeign功能:

5.1.3 编写OpenFeign客户端

cart-service中,定义一个新的接口,编写Feign客户端:

其中代码如下:

// 服务注册的名称
@FeignClient("item-service")
public interface ItemClient {

    @GetMapping("/items")
    List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}

这里只需要声明接口,无需实现方法。接口中的几个关键信息:

  • @FeignClient("item-service") :声明服务名称

  • @GetMapping :声明请求方式

  • @GetMapping("/items") :声明请求路径

  • @RequestParam("ids") Collection<Long> ids :声明请求参数

  • List<ItemDTO> :返回值类型

有了上述信息,OpenFeign就可以利用动态代理帮我们实现这个方法,并且向http://item-service/items发送一个GET请求,携带ids为请求参数,并自动将返回值处理为List<ItemDTO>

只需要直接调用这个方法,即可实现远程调用了。

5.1.4 使用FeignClient

最后,我们在cart-servicecom.hmall.cart.service.impl.CartServiceImpl中改造代码,直接调用ItemClient的方法:

feign替我们完成了服务拉取、负载均衡、发送http请求的所有工作,是不是看起来优雅多了。

而且,这里我们不再需要RestTemplate了,还省去了RestTemplate的注册。

但是底层有一个缺陷就是,没有配置连接池,那我们需要手动来配置。

5.2 连接池

Feign底层发起http请求,依赖于其它的框架。其底层支持的http客户端实现包括:

  • HttpURLConnection:默认实现,不支持连接池

  • Apache HttpClient :支持连接池

  • OKHttp:支持连接池

因此我们通常会使用带有连接池的客户端来代替默认的HttpURLConnection。

比如,我们使用OKHttp。

5.2.1 引入依赖

cart-servicepom.xml中引入依赖:

<!--OK http 的依赖 -->
<dependency>
  <groupId>io.github.openfeign</groupId>
  <artifactId>feign-okhttp</artifactId>
</dependency>

5.2.2 开启连接池

cart-serviceapplication.yml配置文件中开启Feign的连接池功能:

feign:
  okhttp:
    enabled: true # 开启OKHttp功能

重启服务,连接池就生效了。

5.2.3 验证

我们可以打断点验证连接池是否生效,在FeignBlockingLoadBalancerClient中的execute方法中打断点:

Debug方式启动cart-service,请求一次查询我的购物车方法,进入断点:

可以发现这里底层的实现已经改为OkHttpClient。

5.3 最佳实践

将来我们要把与下单有关的业务抽取为一个独立微服务:trade-service。

也就是说,如果拆分了交易微服务(trade-service),它也需要远程调用item-service中的根据id批量查询商品功能。这个需求与cart-service中是一样的。

因此,我们就需要在trade-service中再次定义ItemClient接口,这不是重复编码吗? 有什么办法能加避免重复编码呢?

5.3.1 思路分析

相信大家都能想到,避免重复编码的办法就是抽取。不过这里有两种抽取思路:

  • 思路1:抽取到微服务之外的公共module

  • 思路2:每个微服务自己抽取一个module(自己的接口,自己提供)

如图:

方案1:抽取更加简单,工程结构也比较清晰,但缺点是整个项目耦合度偏高。

方案2:抽取相对麻烦,工程结构相对更复杂,但服务之间耦合度降低。

5.3.2 抽取Feign客户端

hmall下定义一个新的module,命名为hm-api。

其依赖如下:

<?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>hmall</artifactId>
        <groupId>com.heima</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>hm-api</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <!--open feign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- load balancer-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <!-- swagger 注解依赖 -->
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>1.6.6</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
</project>

然后把ItemDTO和ItemClient都拷贝过来,最终结构如下:

现在,任何微服务要调用item-service中的接口,只需要引入hm-api模块依赖即可,无需自己编写Feign客户端了。

5.3.3 扫描包

接下来,我们在cart-servicepom.xml中引入hm-api模块:

  <!--feign模块-->
  <dependency>
      <groupId>com.heima</groupId>
      <artifactId>hm-api</artifactId>
      <version>1.0.0</version>
  </dependency>

删除cart-service中原来的ItemDTO和ItemClient,重启项目,发现报错了:

这里因为ItemClient现在定义到了com.hmall.api.client包下,而cart-service的启动类定义在com.hmall.cart包下,扫描不到ItemClient,所以报错了。

解决办法很简单,在cart-service的启动类上添加声明即可,两种方式:

  • 方式1:声明扫描包:

  • 方式2:声明要用的FeignClient

5.4 日志配置

需要注意的是一般情况下不会打开该日志信息,除非有错误,定位错误的时候会打开。

OpenFeign只会在FeignClient所在包的日志级别为debug时,才会输出日志。

而且其日志级别有4级:

  • NONE不记录任何日志信息,这是默认值。

  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间

  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息

  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。

Feign默认的日志级别就是NONE,所以默认我们看不到请求日志。

5.4.1 定义日志级别

在hm-api模块下新建一个配置类,定义Feign的日志级别:

代码如下:

public class DefaultFeignConfig {
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.FULL;
    }
}

注意事项:这里不需要在类上加@Configration注解。

5.4.2 配置日志级别

接下来,要让日志级别生效,还需要配置这个类。有两种方式:

  • 局部生效:在某个FeignClient中配置,只对当前FeignClient生效

@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)
  • 全局生效:在@EnableFeignClients中配置,针对所有FeignClient生效。

@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)

日志格式:


原文地址:https://blog.csdn.net/2301_77358195/article/details/144741816

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!