SpringCloud微服务框架

一、微服务

1.1 微服务技术栈

SpringCloud微服务框架_第1张图片
微服务,简单来说,就是将很多功能拆分称一个一个的服务集群,同时服务网关进行服务的筛选。注册中心中注册了所有服务的接口,配置中心中集中了各个服务的配置。在此基础上,又进行分布式日志服务,进行日志采集,并使用系统监控,链路追踪技术,找到每个服务的调用链路。其中还涉及到分布式缓存技术。同时,Jenkins加docker等技术,实现了持续集成。

1.2 微服务特征

微服务是一种经过良好架构设计的分布式架构方案,微服务架构特征:

  • 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
  • 面向服务:微服务对外暴露业务接口
  • 自治:团队独立、技术独立、数据独立、部署独立
  • 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题

二、SpringCloud概念

2.1 SpringCloud简介

  1. SpringCloud官网
  2. SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验
    SpringCloud微服务框架_第2张图片

2.2 服务拆分注意事项

  1. 不同微服务,不要重复开发相同业务
  2. 微服务数据独立,不要访问其它微服务的数据库
  3. 微服务可以将自己的业务暴露为接口,供其它微服务调用

2.3 提供者和消费者

  • 服务提供者:暴露接口给其它微服务调用
  • 服务消费者:调用其它微服务提供的接口
  • 提供者与消费者角色其实是相对的,一个服务可以同时是服务提供者和服务消费者

三、Eureka注册中心

3.1 EureKa的作用

  1. 注册服务信息。每一个服务提供者在启动的时候,都会向EureKa注册服务的信息,例如ip,port等。Eureka会保存这些信息
  2. 拉取服务。消费者可以向Eureka拉去其他服务的信息,例如去拿到另一个服务的ip和port等
  3. 负载均衡。在消费者拉去到另一个服务的信息后,通过负载均衡算法选择可用的合适服务器
  4. 远程调用。消费者通过负载均衡选举出了一个可用的ip和port后,通过http请求向另一个服务发送请求,进行远程调用

【注:服务提供者每隔30秒会向Eureka发送心跳,保持健康状态,若健康状态不正常,则将该服务的信息从服务列表中剔除,保证消费者拉取的都是最新的信息】

3.2 Eureka的搭建

pom文件,这里继承了父工程的依赖,父工程中已经引入了springboot,版本需要对应,我这里springboot的版本是2.6.11,对应的springcloud版本是2021.0.3,Eureka的版本由spring版本控制自动匹配。【父工程的pom文件附后】


<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>study_springcloudartifactId>
        <groupId>com.studygroupId>
        <version>0.0.1-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>

    <artifactId>eureka-serverartifactId>

    <properties>
        <maven.compiler.source>8maven.compiler.source>
        <maven.compiler.target>8maven.compiler.target>
    properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
        dependency>
    dependencies>

project>

父工程pom文件


<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <groupId>com.studygroupId>
    <artifactId>study_springcloudartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <modules>
        <module>user-servicemodule>
        <module>order-servicemodule>
        <module>eureka-servermodule>
    modules>

    <packaging>pompackaging>

    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.6.11version>
        <relativePath/>
    parent>

    <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
        <java.version>1.8java.version>
        <spring-cloud.version>2021.0.3spring-cloud.version>
        <mysql.version>5.1.47mysql.version>
        <mybatis.version>2.2.2mybatis.version>
    properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-dependenciesartifactId>
                <version>${spring-cloud.version}version>
                <type>pomtype>
                <scope>importscope>
            dependency>
            
            <dependency>
                <groupId>org.mybatis.spring.bootgroupId>
                <artifactId>mybatis-spring-boot-starterartifactId>
                <version>${mybatis.version}version>
            dependency>
        dependencies>
    dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>${mysql.version}version>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
        dependency>
    dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-compiler-pluginartifactId>
                <version>3.8.1version>
                <configuration>
                    <source>1.8source>
                    <target>1.8target>
                    <encoding>UTF-8encoding>
                configuration>
            plugin>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>

project>

application.yml,我这里定义了Eureka的端口为10086

server:
  port: 10086  # 服务端口
spring:
  application:
    name: eurekaserver
eureka:
  client:
    service-url:  # eureka的地址信息
      defaultZone: http://localhost:10086/eureka

main函数

package com.study.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}

待启动完成后,访问http://localhost:10086,即可看到Eureka界面
SpringCloud微服务框架_第3张图片

3.3 Eureka服务注册

  1. 引入依赖。在服务中引入Eureka的依赖
<dependency>
	<groupId>org.springframework.cloudgroupId>
	<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
  1. 添加配置。在要注册服务的yml文件中,加入Eureka的配置

【主要是spring.application.name和eureka.client.service-url.defaultZone的配置】

server:
  port: 8552
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
    username: root
    password: 123
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: userservice  # user服务的名称
mybatis:
  type-aliases-package: com.study.user.pojo
  configuration:
    map-underscore-to-camel-case: true
eureka:
  client:
    service-url:  # eureka的地址信息
      defaultZone: http://localhost:10086/eureka
  1. 重启服务,在页面中看到已经服务已经注册到eureka
    SpringCloud微服务框架_第4张图片

3.4 Eureka服务发现

  1. 引入依赖
<dependency>
	<groupId>org.springframework.cloudgroupId>
	<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
  1. 添加配置。在要注册服务的yml文件中,加入Eureka的配置

【主要是spring.application.name和eureka.client.service-url.defaultZone的配置】

server:
  port: 8551
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
    username: root
    password: 123
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: orderservice  # order服务的名称
mybatis:
  type-aliases-package: com.study.user.pojo
  configuration:
    map-underscore-to-camel-case: true
eureka:
  client:
    service-url:  # eureka的地址信息
      defaultZone: http://localhost:10086/eureka
  1. 将调用生产者服务的ip和port,替换成服务注册时的注册名
package com.study.order.service;

import com.study.order.mapper.OrderMapper;
import com.study.order.pojo.Order;
import com.study.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.client.RestTemplate;

/**
 * @author wangchaoyang
 * @since 2023/2/14 15:37
 */
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("{orderId}")
    public Order queryOrderById(@PathVariable("orderId") Long orderId) {
        Order order = orderMapper.findById(orderId);
        //原来:String url = "http://localhost:8551/user/" + order.getUserId();
        String url = "http://userservice/user/" + order.getUserId();
        User user = restTemplate.getForObject(url, User.class);
        order.setUser(user);
        return order;
    }
}
  1. 在消费者服务启动类中,创建RestTemplate对象,并加上@LoadBalanced注解
package com.study;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@MapperScan("com.study.order.mapper")
@SpringBootApplication
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }

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

四、Nacos注册中心

4.1 Nacos安装

在Nacos官网,下载安装包。可以在网上搜索下,Springboot版本和Nacos版本的对应关系。

SpringCloud微服务框架_第5张图片

创建nacos_config的MySQL数据库,并执行conf文件夹下的nacos-mysql.sql文件。

添加application.properties文件,并添加如下配置。另外如果想要修改nacos的端口,也可在此文件中修改。

spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=123456

进入bin目录下使用cmd输入 startup.cmd -m standalone 命令,进行单机运行。【注意路径不能有中文】

SpringCloud微服务框架_第6张图片

点击上图中nacos图标旁边的地址,进入浏览器查看页面。登录的默认账号和密码都是nacos。

SpringCloud微服务框架_第7张图片

4.2 Nacos使用

引入依赖,在父工程中的dependencyManagement中添加nacos管理依赖

<dependency>
    <groupId>com.alibaba.cloudgroupId>
    <artifactId>spring-cloud-alibaba-dependenciesartifactId>
    <version>2.2.5.RELEASEversion>
    <type>pomtype>
    <scope>importscope>
dependency>

在各个服务中,添加nacos依赖

<dependency>
    <groupId>com.alibaba.cloudgroupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>

在application.yml文件中,写入nacos配置。主要是spring.cloud.nacos.server-addr的值。

server:
  port: 8552
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
    username: root
    password: 123
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: orderservice  # order服务的名称
  cloud:
      nacos:
        server-addr: localhost:8848  # nacos服务地址
mybatis:
  type-aliases-package: com.study.user.pojo
  configuration:
    map-underscore-to-camel-case: true

分别开启生产者和消费者服务,可以在nacos页面看到注册的服务

SpringCloud微服务框架_第8张图片

4.3 Nacos服务分级存储

  • 一级是服务,例如userservice服务
  • 二级是集群,一个机房的多个实例叫集群,例如杭州集群和上海集群
  • 三级是实例,例如杭州机房的某台部署了userservice的服务器
    SpringCloud微服务框架_第9张图片

4.4 配置实例的集群属性

方法是,修改application.yml文件,添加spring.cloud.nacos.discovery.cluster-name属性。

server:
  port: 8551
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false
    username: root
    password: 123
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: userservice  # user服务的名称
  cloud:
    nacos:
      server-addr: localhost:8848  # nacos服务地址
      discovery:
        cluster-name: HZ   # 集群名称
mybatis:
  type-aliases-package: com.study.user.pojo
  configuration:
    map-underscore-to-camel-case: true

在注册中心页面可以查看到集群名称和实例。

SpringCloud微服务框架_第10张图片

4.5 Nacos根据集群负载均衡

4.5.1 案例演示

现在给userservice配置集群HZ,开启两个实例;给userservice配置集群SH,开启一个实例。给orderservice配置集群HZ,开启一个实例。

userservice配置两个集群,三个实例

SpringCloud微服务框架_第11张图片

orderservice配置一个集群,一个实例

SpringCloud微服务框架_第12张图片

要做到的是,在HZ集群的orderservice远程调用userservice服务的时候,只访问HZ集群内的userservice,即只访问本地集群,因为跨集群访问的速度会很慢,不建议跨集群访问。

方法:在orderservice中的application.yml文件中,添加配置user.ribbon.NFLoadBalancerRuleClassName即可。

server:
  port: 8551
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
    username: root
    password: 123
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: orderservice  # order服务的名称
  cloud:
    nacos:
      server-addr: localhost:8848  # nacos服务地址
      discovery:
        cluster-name: HZ  # 集群名称
mybatis:
  type-aliases-package: com.study.user.pojo
  configuration:
    map-underscore-to-camel-case: true
userservice:
  ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule  # nacos负载均衡规则

4.5.2 NacosRule负载均衡策略

  • NacosRule优先选择同集群服务实例列表
  • 当本地集群提供者服务都挂掉了,才会找其他集群访问,并且会报警告信息
  • 确定了可用实例列表后,再使用随机负载均衡策略,挑选实例

4.5.3 Nacos加权负载均衡

  • 在nacos控制台,可以设置实例的权重值,0~1之间
  • 同集群内的多个实例,权重越高,被访问的频率越高
  • 权重设置为0时,则完全不会被访问

4.6 Nacos环境隔离

Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西, 用来做最外层隔离。不通namespace中的服务互不可见。
SpringCloud微服务框架_第13张图片

可以在Nacos的页面中,创建命名空间

SpringCloud微服务框架_第14张图片

此时,会生成一个唯一的命名空间ID

SpringCloud微服务框架_第15张图片

可以看到,dev命名空间下没有服务

SpringCloud微服务框架_第16张图片

在服务中添加配置,主要是spring.cloud.nacos.discovery.namespace参数

server:
  port: 8551
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
    username: root
    password: 123
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: orderservice  # order服务的名称
  cloud:
    nacos:
      server-addr: localhost:8848  # nacos服务地址
      discovery:
        cluster-name: HZ  # 集群名称
        namespace: 23312037-1af0-4fd3-b146-25cf8de5d839  # 命名空间-dev环境
mybatis:
  type-aliases-package: com.study.user.pojo
  configuration:
    map-underscore-to-camel-case: true
userservice:
  ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule  # nacos负载均衡规则

五、Nacos配置管理

5.1 Nacos配置

Nacos不仅可以当注册中心,也可以当配置中心。就是将服务中的一些配置,放置到nacos中管理,这里配置往往是平时需要修改的配置。

在Nacos的配置管理中,点击“+”进行添加配置。

SpringCloud微服务框架_第17张图片

SpringCloud微服务框架_第18张图片

5.2 Nacos如何加载配置

在项目启动后,会先读取Nacos中配置文件的内容,再读取本地application.yml文件中的配置,将两者合并后使用。

将Nacos配置文件的地址放bootstrap.yml配置文件中,因为如果放到application.yml中,这个文件在Nacos配置文件后加载,所以读取不到。而bootstrap.yml文件的优先级高于application.yml,所以放在bootstrap.yml中。

5.3 Nacos管理配置的步骤

1. 在Nacos中添加配置文件,如5.1所示

2. 在微服务中引入依赖


<dependency>
	<groupId>com.alibaba.cloudgroupId>
	<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>

<dependency>
	<groupId>org.springframework.cloudgroupId>
	<artifactId>spring-cloud-starter-bootstrapartifactId>
dependency>

3. 创建bootstrap.yaml文件,并写入配置

主要写服务名称,环境,后缀名,来找到Nacos配置的名称

spring:
  application:
    name: userservice
  profiles:
    active: dev  # 环境
  cloud:
    nacos:
      server-addr: localhost:8848  # nacos地址
      config:
        file-extension: yaml  # 文件后缀名

4. 删除application.yml中的重复配置

5. 通过@Value注解,可以获取到Nacos配置中的值

package com.study.user.web;

import com.study.user.pojo.User;
import com.study.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    // 获取Nacos配置中的值
    @Value("${pattern.dateformat}")
    private String dateformat;

    @GetMapping("/{id}")
    public User quertById(@PathVariable("id") Long id,
                          @RequestHeader(value = "Truth", required = false) String truth) {
        System.out.println("truth:" + truth);
        return userService.queryById(id);
    }

    @GetMapping("/now")
    public String now() {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }
}

6. 开启服务,访问http://localhost:8552/user/now请求,可以获取到Nacos配置中的值.

5.4 Nacos热更新

当Nacos配置文件更新时,Nacos可以支持不重启项目,就能热更新这些更新的配置。

方式一:在@Value注入的变量所在类上添加注解@RefreshScope

package com.study.user.web;

import com.study.user.pojo.User;
import com.study.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {

    @Autowired
    private UserService userService;

    @Value("${pattern.dateformat}")
    private String dateformat;

    @GetMapping("/{id}")
    public User quertById(@PathVariable("id") Long id,
                          @RequestHeader(value = "Truth", required = false) String truth) {
        System.out.println("truth:" + truth);
        return userService.queryById(id);
    }

    @GetMapping("/now")
    public String now() {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }
}

方式二【推荐】:通过配置类进行加载

创建PatternProperties类,通过@ConfigurationProperties注解的profix匹配配置中的前缀,类里面的属性值为profix下面的值。

package com.study.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {

    private String dateformat;
}

获取配置时,通过注入类,调用类的getter方法获取值即可。

package com.study.user.web;

import com.study.config.PatternProperties;
import com.study.user.pojo.User;
import com.study.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;
    
    @Autowired
    private PatternProperties patternProperties;

    @GetMapping("/{id}")
    public User quertById(@PathVariable("id") Long id,
                          @RequestHeader(value = "Truth", required = false) String truth) {
        System.out.println("truth:" + truth);
        return userService.queryById(id);
    }

    @GetMapping("/now")
    public String now() {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));
    }
}

5.5 Nacos多环境共享

在多服务中,如果Nacos的配置中有相同的部分,可以抽取出来,放到[服务名].yaml文件中。

SpringCloud微服务框架_第19张图片

  1. [服务名]-[spring.profile.active].yaml,为环境配置
  2. [服务名].yaml,为默认配置,多环境共享
  3. 优先级:[服务名]-[环境].yaml > [服务名].yaml > 本地配置

5.6 Nacos集群配置

5.6.1 开启Nacos集群

  1. 更改nacos/conf下的cluster.conf.example文件名为cluster.conf,并修改cluster.conf配置文件,增加集群的ip和port
    SpringCloud微服务框架_第20张图片
    注意这里的ip和port,三台集群的端口号要相差2,相差1会出现端口被占用问题,是因为Nacos2以上版本,会增加gRPC端口,会有1000和1001的偏移量

    127.0.0.1:8842
    127.0.0.1:8844
    127.0.0.1:8846
    
  2. 修改每个Nacos的端口,分别为配置的三个端口号【这里以8842为例】

    SpringCloud微服务框架_第21张图片

  3. 分别启动三个Nacos,在每个Nacos的bin目录下,进入命令提示符敲命令startup.cmd即可。其中一个启动的界面如下

    SpringCloud微服务框架_第22张图片

5.6.2 Nginx负载均衡

1. 下载nginx的包,修改conf/nginx.conf文件配置,将如下代码添加到http块中

upstream nacos-cluster是让nginx对这个集群做负载均衡,集群中有三台机器;server是监听81端口,location /nacos是当访问localhost:81/nacos的时候,nginx会代理到集群中去。

    upstream nacos-cluster {
        server 127.0.0.1:8842;
        server 127.0.0.1:8844;
        server 127.0.0.1:8846;
    }

    server {
        listen       81;
        server_name  localhost;

        location /nacos {
            proxy_pass http://nacos-cluster;
        }
    }

2. 在nginx目录下,start nginx.exe启动即可
3. 将bootstrap.yaml文件中的nacos地址,配置为负载地址

spring:
  application:
   name: userservice
  profiles:
    active: dev  # 环境
  cloud:
    nacos:
      server-addr: localhost:81  # nacos负载地址
      config:
        file-extension: yaml  # 文件后缀名

4. 这里启动两台服务,在浏览器访问http://localhost:81/nacos,打开nacos页面

可以看到,Nacos健康实例为2个,集群搭建成功。

SpringCloud微服务框架_第23张图片

六、Feign远程调用

6.1 Feign使用

Feign是一种远程调用的方式,前几章节我们远程调用,是用RestTemplate发起http请求,但代码可读性差,url维护不方便,使用Feign来解决这些问题。

  1. 添加依赖
<dependency>
	<groupId>org.springframework.cloudgroupId>
	<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
	<groupId>org.springframework.cloudgroupId>
	<artifactId>spring-cloud-starter-loadbalancerartifactId>
dependency>

因为ribbon的负载均衡会和Feign的相冲突,所以要排出ribbon依赖,如下

<dependency>
	<groupId>com.alibaba.cloudgroupId>
	<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
<exclusions>
    <exclusion>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-netflix-ribbonartifactId>
    exclusion>
exclusions>
dependency>
  1. 在OrderService启动类中添加注解@EnableFeignClients
package com.study;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@MapperScan("com.study.order.mapper")
@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }

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

  1. 创建接口,接口中包含请求参数、请求方式、请求路径和返回值类型
package com.study.order.clients;

import com.study.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient("userservice")
public interface UserClient {

    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

  1. 在service中用FeignClinet中定义的方法,去代替RestTemplate请求的形式即可
package com.study.order.service;

import com.study.order.clients.UserClient;
import com.study.order.mapper.OrderMapper;
import com.study.order.pojo.Order;
import com.study.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.client.RestTemplate;

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private UserClient userClient;

    public Order queryOrderById(@PathVariable("orderId") Long orderId) {
        Order order = orderMapper.findById(orderId);
        User user = userClient.findById(order.getUserId());
        order.setUser(user);
        return order;
    }
}

6.2 Feign自定义配置

Feign运行自定义配置来覆盖默认配置,可以修改的配置如下:

SpringCloud微服务框架_第24张图片

方式一:配置文件方式

在OrderService的配置文件中,添加Feign自定义配置

feign.client.config.default.loggerLevel配置是全局配置,如果default改成某个服务的名称,则是针对某个微服务的配置

server:
  port: 8551
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
    username: root
    password: 123
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: orderservice  # order服务的名称
  cloud:
    nacos:
      server-addr: localhost:8848  # nacos服务地址
      discovery:
        cluster-name: HZ  # 集群名称
mybatis:
  type-aliases-package: com.study.user.pojo
  configuration:
    map-underscore-to-camel-case: true
userservice:
  ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule  # nacos负载均衡规则
logging:
  level:
    com.study: debug
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
feign:
  client:
    config:
      default:
        loggerLevel: FULL

方式二:Java代码方式

创建Feign配置类

package com.study.order.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;

public class DefaultFeignConfiguration {

    @Bean
    public Logger.Level logLevel() {
        return Logger.Level.BASIC;
    }
}

如果这个配置仅想某个服务有效,就在某个服务上添加@FeignClient 的configuration参数

package com.study.order.clients;

import com.study.order.config.DefaultFeignConfiguration;
import com.study.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration.class)
public interface UserClient {

    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

如果想让配置全局有效,那么可以在启动类上添加@EnableFeignClients的configuration参数

package com.study;

import com.study.order.config.DefaultFeignConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@MapperScan("com.study.order.mapper")
@SpringBootApplication
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }

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

6.3 Feign优化

  1. 日志级别开启到BASIC
  2. 发送远程调用时吗,使用httpclient连接池代替原生URLConnection【步骤如下】

引入依赖


<dependency>
	<groupId>io.github.openfeigngroupId>
	<artifactId>feign-httpclientartifactId>
dependency>

在application.yaml文件中添加配置

server:
  port: 8551
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
    username: root
    password: 123
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: orderservice  # order服务的名称
  cloud:
    nacos:
      server-addr: localhost:8848  # nacos服务地址
      discovery:
        cluster-name: HZ  # 集群名称
mybatis:
  type-aliases-package: com.study.user.pojo
  configuration:
    map-underscore-to-camel-case: true
userservice:
  ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule  # nacos负载均衡规则
logging:
  level:
    com.study: debug
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
feign:
  client:
    config:
      default:
        loggerLevel: BASIC
  httpclient:
    enable: true  # 支持httpClint开关
    max-connections: 200  # 最大连接数
    max-connections-per-route: 50  # 单个路径的最大连接数

6.4 Feign最佳实践

将FeignClient抽取为独立模块,并且把有关的pojo、默认的Feign配置都放到这个模块中,提供给所有消费者使用。

  1. 新建一个模块,命名为feign-api,然后引入feign的starter依赖

  2. 将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中

    SpringCloud微服务框架_第25张图片

  3. 在order-service中引入feign-api依赖

    SpringCloud微服务框架_第26张图片

  4. 当前定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用,可以在启动类上指定FeignClient的字节码@EnableFeignClients(clients = UserClient.class)

package com.study.order;

import com.study.feign.clients.UserClient;
import com.study.feign.config.DefaultFeignConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@MapperScan("com.study.order.mapper")
@SpringBootApplication
@EnableFeignClients(clients = UserClient.class, defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }

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

七、Gateway网关

7.1 网关的功能

  • 对用户请求做身份认证和权限校验
  • 将用户请求路由到服务器,并实现负载均衡
  • 对用户请求做限流

7.2 搭建网关服务

创建gateway的模块,并修改pom.xml文件


<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>com.study.gatewaygroupId>
    <artifactId>gatewayartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>gatewayname>
    <description>gatewaydescription>

    <parent>
        <artifactId>study_springcloudartifactId>
        <groupId>com.studygroupId>
        <version>0.0.1-SNAPSHOTversion>
    parent>

    <properties>
        <java.version>1.8java.version>
    properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starterartifactId>
        dependency>
        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-gatewayartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-loadbalancerartifactId>
        dependency>
    dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-compiler-pluginartifactId>
                <version>3.8.1version>
                <configuration>
                    <source>1.8source>
                    <target>1.8target>
                    <encoding>UTF-8encoding>
                configuration>
            plugin>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>

project>

创建application.yaml文件,修改里面的网关配置

server:
  port: 10010
logging:
  level:
    com.study: debug
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 120.53.238.31:8848 # nacos地址
    gateway:
      routes:
        - id: user-service          # 路由标识,必须唯一
          uri: lb://userservice     # 路由的目标地址
          predicates:               # 路由断言,判断请求是否符合规则
            - Path=/user/**         # 路径断言,判断路径是否是以/user开头,如果是则符合
        - id: order-service
          uri: lb://orderservice
          predicates:
            - Path=/order/**

启动网关服务

package com.study.gateway.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }

}

7.3 路由配置

  1. 路由id:路由的唯一标识
  2. 路由目标(uri):路由的目标地址,http表示固定地址,lb表示根据服务名负载均衡
  3. 路由断言(predicates):判断路由的规则
  4. 路由过滤器(filter):对请求或响应做处理

7.4 路由断言工厂

  • 我们在配置文件中写的断言规则只是个字符串,这些字符串会被Predictes Factory读取并处理,转变为路由判断的条件
  • 例如,Path=/user/**是按照路径批匹配,这个规则是由org.springframework.cloud.gateway.handler.predicate. PathRoutePredicateFactory类来处理的
  • spring提供了11中基本的Predicate工厂
    SpringCloud微服务框架_第27张图片

7.5 路由过滤器

GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求微服务返回的响应做处理。
SpringCloud微服务框架_第28张图片
spring提供了31种不同的路由过滤器工厂(详细可见spring官网)

SpringCloud微服务框架_第29张图片

在配置文件中某个路由下加入filter配置,指定这个路由的过滤规则;要统一指定所有路由的过滤器,则可以在routes下增加default-filter配置

server:
  port: 10010
logging:
  level:
    com.study: debug
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 120.53.238.31:8848 # nacos地址
    gateway:
      routes:
        - id: user-service          # 路由标识,必须唯一
          uri: lb://userservice     # 路由的目标地址
          predicates:               # 路由断言,判断请求是否符合规则
            - Path=/user/**         # 路径断言,判断路径是否是以/user开头,如果是则符合
          filters:
            - AddRequestHeader=Truth, Hello World!
        - id: order-service
          uri: lb://orderservice
          predicates:
            - Path=/order/**
#      default-filter:
#        - AddRequestHeader=Truth, All!

7.6 GlobalFilter全局过滤器

  • 全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样
  • 区别在于GatewayFilter通过配置定义,处理逻辑是固定的。而GlobalFilter的逻辑需要自 己写代码实现
  • 定义方式是实现GlobalFilter接口

7.7 过滤器的执行顺序

  • 请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、 GlobalFilter
  • 请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter, 合并到一个过滤器链(集合)中,排序后依次执行每个过滤器

八、Docker

8.1 Docker如何解决依赖兼容问题

  • Docker将程序、lib(系统函数库)、Dep(依赖)和配置与应用一起打包,形成可移植的镜像,迁移到任意Linux操作系统
  • 将每个应用放在一个隔离容器里去运行,使用沙箱机制,避免互相干扰

8.2 镜像和容器

  • 镜像(Image):Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像。
  • 容器(Container) :镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器做隔离,对外不可见。

8.3 Docker架构

Docker是一个CS架构,由两部分组成

  • 服务端(server):Docker守护进程,负责处理Docker指令,管理镜像、容器等
  • 客户端(cilient):通过命令或RestAPI向docker服务端发指令,可以在本地或远程向服务端发送指令

docker build指令,会被docker daemon守护进程接收和处理,由它将提供的数据,构建成一个镜像;
docker pull指令,会被docker daemon守护进程接收和处理,由它进行去Registry服务端拉取指定的镜像;
docker run指令,会被docker daemon守护进程接收和处理,告诉server要去创建容器,这时守护进程就会帮助创建容器。

SpringCloud微服务框架_第30张图片

DockerHub是一个镜像托管的服务器,类似的还有阿里云镜像服务,统称为DockerRegistry

8.4 Docker安装

Docker 分为 CE 和 EE 两大版本。CE 即社区版(免费,支持周期 7 个月),EE 即企业版,强调安全,付费使用,支持周期 24 个月。

Docker CE 分为 stable testnightly 三个更新频道。

先卸载原有的Docker

yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-selinux \
                  docker-engine-selinux \
                  docker-engine \
                  docker-ce

安装Docker,使用yum安装

  1. 更新本地镜像源

    # 设置docker镜像源
    yum-config-manager \
        --add-repo \
        https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
        
    sed -i 's/download.docker.com/mirrors.aliyun.com\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo
    
    yum makecache fast
    
  2. 安装Docker

    yum install -y docker-ce
    
  3. 在启动Docker前,关闭防火墙

    systemctl start docker
    
  4. 查看Docker版本

    docker -v
    
  5. 配置镜像加速,针对docker客户端版本大于1.10.0的用户

    sudo mkdir -p /etc/docker
    sudo tee /etc/docker/daemon.json <<-'EOF'
    {
    	"registry-mirrors": ["https://n0dwemtq.mirror.aliyuncs.com"]
    }
    EOF
    sudo systemctl daemon-reload
    sudo systemctl restart docker
    

8.5 Docker镜像操作命令

镜像名称一般分为两部分:[repository]:[tag],在没有指定tag时,默认是latest,代表最新版本镜像

8.5.1 从DockerHub上拉取镜像

打开DockerHub网站,在搜索栏搜索想要的镜像并选择其中一个

SpringCloud微服务框架_第31张图片
上面会有指令:docker pull nginx,在linux下执行这个命令

SpringCloud微服务框架_第32张图片
使用docker images查看本地所有的镜像

SpringCloud微服务框架_第33张图片

8.5.2 将镜像打包成一个文件

使用docker save -o AA.tar nginx:latest命令,进行对nginx:latest镜像进行打包,其中AA.tar是定义打出包的包名,nginx:latest是将哪个镜像进行打包

8.5.3 删除一个镜像

使用docker rmi nginx:latest进行删除nginx:latest这个镜像,也可以通过docker rmi 镜像id进行删除

8.5.4 将一个镜像文件导入

使用docker load -i xxxx这个命令,来将xxxx镜像导入

SpringCloud微服务框架_第34张图片

8.6 Docker容器相关命令

8.6.1 创建运行一个容器

命令:docker run --name containerName -p 80:80 -d nginx

命令解读:

  • docker run:创建并运行一个容器
  • - -name:给容器起名
  • - p:将宿主机端口与容器端口进行映射,冒号左边是宿主机端口,右边是容器端口
  • - d:后台运行容器
  • nginx:镜像名称,例如nginx

8.6.2 查看容器

命令:docker ps查看运行中的容器

在这里插入图片描述

命令:docker ps -a查看所有容器

在这里插入图片描述

8.6.3 查看容器的日志

命令:docker logs -f 容器名

命令解读: -f 参数意思是持续跟踪日志

8.6.4 启动/停止/删除一个容器

命令:docker start 容器名启动一个容器

命令:docker stop 容器名停止一个容器的运行

命令:docker rm 容器名删除一个容器,加上-f参数是强制删除运行中的容器

8.6.5 进入容器内部

命令:docker exec -it 容器名 bash

命令解读:

  • docker exec:进入容器内部,执行一个命令
  • -it:给当前进入的容器创建一个标准输入、输出终端,允许我们与容器交互
  • bash:进入容器后执行的命令,bash是一个linux终端交互命令

8.7 Dockerfile自定义镜像

8.7.1 镜像分层

  • 镜像是分层结构,每一层称为一个Layer
  • Baselmage层:包含基本的系统函数库、 环境变量、文件系统
  • Entrypoint:入口,是镜像中应用启动的命令
  • 其它:在Baselmage基础上添加依赖、安装程序、完成整个应用的安装和配置

8.7.2 Dockerfile

Dockerfile就是一个文本文件,其中包含一个个指令,用指令来说明要执行什么操作来构建镜像,每一个指令都会形成一层layer

SpringCloud微服务框架_第35张图片

九、MQ消息队列

9.1 同步调用

同步调用就是按顺序一个一个调用服务,等待上一个服务调用完成后,才开始调用下一个服务,时效性强,可以立即得到结果。

同步调用的问题:

  • 耦合度高,酶促加入新需求,都会修改原来代码
  • 性能下降,调用者需要等待服务提供者响应,如果调用链过长,则响应时间就等于每次调用时间的总和
  • 资源浪费,调用链中的每个服务在等待响应的过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源
  • 级联失败,服务提供者出现问题,所有调用方都会出问题

9.2 异步调用

异步调用会设置一个Broker,由它来管理服务。当服务调用者请求Broker时候,Broker会向它管理的服务发布消息,然后这些收到消息的服务就会去处理相关业务。前提是Broker管理的服务要先订阅Broker,只有订阅了Broker的服务才能收到Broker发布的消息。

异步通信优点:

  • 耦合度低
  • 吞吐量提升
  • 故障隔离
  • 流量削峰

异步通信缺点:

  • 依赖于Broker的可靠性、安全性和吞吐能力
  • 架构复杂,业务没有明显的流程线,不好追踪管理

9.3 什么是MQ

MQ即MessageQueue,消息队列,也就是Broker

SpringCloud微服务框架_第36张图片

9.4 安装RabbitMQ

利用Docker在线拉取镜像

docker pull rabbitmq:3-management

安装启动MQ

docker run -e RABBITMQ_DEFAULT_USER=root -e RABBITMQ_DEFAULT_PASS=123 --name mq --hostname mq1 -p 15672:15672 -p 5672:5672 -d rabbitmq:3-management

其中,15672端口是管理端界面的访问端口,5672是后续通讯端口。启动MQ后,可以使用ip:15672来访问管理端界面,账号就是上面设置的root,密码就是123

SpringCloud微服务框架_第37张图片
channel:操作MQ的工具
exchange:路由消息到队列中
queue:缓存消息
virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组

9.5 SpringAMQP

9.5.1 什么是SpringAMQP

Advanced Message Queuing Protocol,是用于在应用程序或之间传递业务消息的开放标准,该协议与语言和平台无关,更符合微服务中独立性的要求。

SpringAMQP是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中Spring-amqp是基础抽象,Spring-rabbit是底层的默认实现。

9.5.2 使用SpringAMQP发送消息

  1. 在父工程中引入spring-amqp依赖

    <dependency>
    	<groupId>org.springframework.bootgroupId>
    	<artifactId>spring-boot-starter-amqpartifactId>
    dependency>
    
  2. 配置SpringAMQP,在application.yml中配置相关配置

    spring:
      rabbitmq:
        host: 8.130.111.12
        port: 5672
        username: root
        password: xxxxxx
        virtual-host: /
    
  3. 在publisher服务中利用RabbitTemplate发送消息到simple.queue队列

    package com.study.publisher.spring;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class SpringAmqpTest {
    
        @Autowired
        private RabbitTemplate rabbitTemplate;
    
        @Test
        public void testSendMessage2SimpleQueue() {
            String queueName = "simple.queue";
            String message = "hello, spring amqp";
            rabbitTemplate.convertAndSend(queueName, message);
        }
    }
    
    
  4. 在管理端查看发布的消息

    SpringCloud微服务框架_第38张图片

9.5.3 使用SpringAMQP消费消息

  1. 配置SpringAMQP,在application.yml中配置相关配置

    spring:
      rabbitmq:
        host: 8.130.111.12
        port: 5672
        username: root
        password: xxxxxx
        virtual-host: /
    
  2. 在consumer中编写消费逻辑,使用@RabbitListener注解指定队列名称,就能监听simple.queue,接收消息了

    package com.study.consumer.listener;
    
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.stereotype.Component;
    
    @Component
    public class SpringRabbitListener {
    
        @RabbitListener(queues = "simple.queue")
        public void listenSimpleQueue(String msg) {
            System.out.println("消费者接收到simple.queue消息:" + msg);
        }
    }
    
  3. 启动consumer后,发现消息已经被消费了

    SpringCloud微服务框架_第39张图片
    SpringCloud微服务框架_第40张图片

9.6 SpringAMQP之Work Queue工作队列

Work Queue,工作队列,可以提高消息处理速度,避免队列消息堆积

SpringCloud微服务框架_第41张图片

  1. 在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue

    package com.study.publisher.spring;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class SpringAmqpTest {
    
        @Autowired
        private RabbitTemplate rabbitTemplate;
    
        @Test
        public void testSendMessage2WorkQueue() throws InterruptedException {
            String queueName = "simple.queue";
            String message = "hello, spring__";
            for (int i = 0; i < 50; i++) {
                rabbitTemplate.convertAndSend(queueName, message + i);
                Thread.sleep(20);
            }
        }
    }
    
    
  2. 在consumer服务中定义两个消息监听者,都监听simple.queue队列,消费者1每秒处理50条消息,消费者2每秒处理10条消息

    package com.study.consumer.listener;
    
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.stereotype.Component;
    
    import java.time.LocalTime;
    
    @Component
    public class SpringRabbitListener {
    
        @RabbitListener(queues = "simple.queue")
        public void listenWorkQueue1(String msg) throws InterruptedException {
            System.out.println("消费者1接收到simple.queue消息:【" + msg + "】" + LocalTime.now());
            Thread.sleep(20);
        }
    
        @RabbitListener(queues = "simple.queue")
        public void listenWorkQueue2(String msg) throws InterruptedException {
            System.out.println("消费者2........接收到simple.queue消息:【" + msg + "】" + LocalTime.now());
            Thread.sleep(200);
        }
    }
    
    
  3. 我们发现,消费者1会处理偶数条的消息,而消费者2会处理奇数条的消息,这是消息预取导致的,消费者2处理的慢,但也会去取偶数条的所有消息,导致消费者1处理完所有奇数消息后就不处理了,造成了资源浪费

    SpringCloud微服务框架_第42张图片

  4. 消费预取限制。修改application.yml文件,设置prefetch值,设置消息预取为1,意思是每次只取一条消息,消费完了后再取。

    spring:
      rabbitmq:
        host: 8.130.111.12
        port: 5672
        username: root
        password: xxxxxx
        virtual-host: /
        listener:
          simple: 
            prefetch: 1  # 消息预取
    

9.7 SpringAMQP发布、订阅模型

发布订阅模型与之前的区别在于,允许将同一消息发送给多个消费者,实现方式是加入了exchange交换机

SpringCloud微服务框架_第43张图片
exchange类型包括:Fanout广播、Direct路由、Topic话题

9.7.1 发布订阅Fanout Exchange

Fanout Exchange会将接收到的消息路由到每一个跟其绑定的queue

  1. 在consumer服务中,利用代码声明队列、交换机,并将两者绑定

    package com.study.consumer.config;
    
    import org.springframework.amqp.core.Binding;
    import org.springframework.amqp.core.BindingBuilder;
    import org.springframework.amqp.core.FanoutExchange;
    import org.springframework.amqp.core.Queue;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class FanoutConfig {
    
        // 声明交换机 itcast.fanout
        @Bean
        public FanoutExchange fanoutExchange() {
            return new FanoutExchange("itcast.fanout");
        }
    
        // 声明队列1 fanout.queue1
        @Bean
        public Queue fanoutQueue1() {
            return new Queue("fanout.queue1");
        }
    
        // 声明队列2 fanout.queue1
        @Bean
        public Queue fanoutQueue2() {
            return new Queue("fanout.queue2");
        }
    
        // 绑定队列1到交换机
        @Bean
        public Binding fanoutBinding1(Queue fanoutQueue1, FanoutExchange fanoutExchange) {
            return BindingBuilder
                    .bind(fanoutQueue1)
                    .to(fanoutExchange);
        }
    
        // 绑定队列2到交换机
        @Bean
        public Binding fanoutBinding2(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
            return BindingBuilder
                    .bind(fanoutQueue2)
                    .to(fanoutExchange);
        }
    }
    
    
  2. 在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2

    package com.study.consumer.listener;
    
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.stereotype.Component;
    
    @Component
    public class SpringRabbitListener {
    
        @RabbitListener(queues = "fanout.queue1")
        public void listenFanoutQueue1(String msg) {
            System.out.println("消费者接收到fanout.queue1消息:【" + msg + "】");
        }
    
        @RabbitListener(queues = "fanout.queue2")
        public void listenFanoutQueue2(String msg) {
            System.out.println("消费者接收到fanout.queue2消息:【" + msg + "】");
        }
    }
    
  3. 在publisher中编写测试方法,向itcast.fanout发送消息

    package com.study.publisher.spring;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class SpringAmqpTest {
    
        @Autowired
        private RabbitTemplate rabbitTemplate;
    
        @Test
        public void testSendFanoutExchange() {
            // 交换机名称
            String exchangeName = "itcast.fanout";
            // 消息
            String message = "hello, everyone!";
            // 发送消息
            rabbitTemplate.convertAndSend(exchangeName, "", message);
        }
    }
    
  4. 测试,发现两个队列均收到了消息

    SpringCloud微服务框架_第44张图片

9.7.2 发布订阅Direct Exchange

  • Direct Exchange会将接收到的消息根据规则路由到指定的Queue,因此称为路由模式
  • 每一个Queue都与Exchange设置一个BindingKey
  • 发布者发送消息时,指定消息的RoutingKey
  • Exchange将消息路由到BindingKey与消息RoutingKey一致的队列

SpringCloud微服务框架_第45张图片

  1. 消费者consumer编写两个消费者方法,分别监听direct.queue1和direct.queue2,并利用@RabbitListener声明Exchange、Queue、RoutingKey

    package com.study.consumer.listener;
    
    import org.springframework.amqp.core.ExchangeTypes;
    import org.springframework.amqp.rabbit.annotation.Exchange;
    import org.springframework.amqp.rabbit.annotation.Queue;
    import org.springframework.amqp.rabbit.annotation.QueueBinding;
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.stereotype.Component;
    
    @Component
    public class SpringRabbitListener {
    
        @RabbitListener(bindings = @QueueBinding(
                value = @Queue(name = "direct.queue1"),
                exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
                key = {"red", "blue"}
        ))
        public void listenerDirectQueue1(String msg) {
            System.out.println("消费者接收到direct.queue1消息:【" + msg + "】");
        }
    
        @RabbitListener(bindings = @QueueBinding(
                value = @Queue(name = "direct.queue2"),
                exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
                key = {"red", "yellow"}
        ))
        public void listenerDirectQueue2(String msg) {
            System.out.println("消费者接收到direct.queue2消息:【" + msg + "】");
        }
    }
    
    
  2. 在publisher服务的测试类中添加测试方法

    package com.study.publisher.spring;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class SpringAmqpTest {
    
        @Autowired
        private RabbitTemplate rabbitTemplate;
    
        @Test
        public void testSendDirectExchange() {
            // 交换机名称
            String exchangeName = "itcast.direct";
            // 消息
            String message = "hello, red!";
            // 发送消息
            rabbitTemplate.convertAndSend(exchangeName, "red", message);
        }
    }
    
    

9.7.3 Direct和Fanout交换机的差异

  • Fanout交换机将消息路由给每一个与之绑定的队列
  • Direct交换机根据RoutingKey判断路由给哪个队列
  • 如果多个队列具有相同的RoutingKey,则与Fanout功能类似

9.7.4 发布订阅Topic Exchange

  • TopicExchange与DirectExchange类似,区别在于routingKey必须是多个单词的列表,并且以.分割

  • Queue与Exchange指定BindingKey时可以使用通配符

    • # :代表0个或多个单词
    • * :代表1个单词
      SpringCloud微服务框架_第46张图片
  1. 利用@RabbitListener声明Exchange、Queue、RoutingKey,在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2

    package com.study.consumer.listener;
    
    import org.springframework.amqp.core.ExchangeTypes;
    import org.springframework.amqp.rabbit.annotation.Exchange;
    import org.springframework.amqp.rabbit.annotation.Queue;
    import org.springframework.amqp.rabbit.annotation.QueueBinding;
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.stereotype.Component;
    
    @Component
    public class SpringRabbitListener {
    
        @RabbitListener(bindings = @QueueBinding(
                value = @Queue(name = "topic.queue1"),
                exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
                key = "china.#"
        ))
        public void listenerTopicQueue1(String msg) {
            System.out.println("消费者接收到topic.queue1消息:【" + msg + "】");
        }
    
        @RabbitListener(bindings = @QueueBinding(
                value = @Queue(name = "topic.queue2"),
                exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
                key = "#.news"
        ))
        public void listenerTopicQueue2(String msg) {
            System.out.println("消费者接收到topic.queue2消息:【" + msg + "】");
        }
    }
    
    
  2. 在publisher中编写测试方法,向itcast.topic发送消息

    package com.study.publisher.spring;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class SpringAmqpTest {
    
        @Autowired
        private RabbitTemplate rabbitTemplate;
    
        @Test
        public void testSendTopicExchange() {
            // 交换机名称
            String exchangeName = "itcast.topic";
            // 消息
            String message = "这是一条新闻!";
            // 发送消息
            rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
        }
    }
    
    
  3. 启动后,可以看到满足条件的queue1和queue2都收到了消息

    SpringCloud微服务框架_第47张图片

9.8 消息转换器

  • Spring的对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的
  • 默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化
  • 如果要修改只需要定义一个MessageConverter类型的Bean即可,推荐用JSON方式序列化
  1. 声明一个队列

        @Bean
        public Queue objectQueue() {
            return new Queue("object.queue");
        }
    
  2. 消息发送方发送Map类型的参数

        @Test
        public void testSendObjectQueue() {
            Map<String, Object> msg = new HashMap<>();
            msg.put("name", "张三");
            msg.put("age", 21);
            rabbitTemplate.convertAndSend("object.queue", msg);
        }
    
  3. 会发现消息无法识别成Map集合类型

    SpringCloud微服务框架_第48张图片

  4. 修改序列化为json形式,首先引入依赖

    <dependency>
    	<groupId>com.fasterxml.jackson.coregroupId>
    	<artifactId>jackson-databindartifactId>
    dependency>
    
  5. 序列化为json

    package com.study.publisher;
    
    import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
    import org.springframework.amqp.support.converter.MessageConverter;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.Bean;
    
    @SpringBootApplication
    public class PublisherApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(PublisherApplication.class, args);
        }
    	
    	// 序列化
        @Bean
        public MessageConverter messageConverter() {
            return new Jackson2JsonMessageConverter();
        }
    }
    
  6. 再次发送消息,查看已经序列化为json

    SpringCloud微服务框架_第49张图片

  7. 消息接收

        @RabbitListener(queues = "object.queue")
        public void listenObjectQueue(Map<String, Object> msg) {
            System.out.println("接收到object.queue消息:" + msg);
        }
    

    在这里插入图片描述

你可能感兴趣的:(spring,cloud,微服务,eureka)