SpringCloud学习笔记

目录

一、认识微服务

单体架构

分布式架构

微服务

SpringCloud

内容知识

技术栈对比

服务拆分

远程调用

二、Eureka注册中心

搭建注册中心

服务注册

服务拉取

三、Ribbon负载均衡

源码跟踪

流程总结

负载均衡策略

自定义策略

饥饿加载

四、Nacos注册中心

服务注册

分级存储模型

配置集群

NacosRule

权重配置

环境隔离

创建namespace

配置namespace

临时实例

五、Nacos配置中心

创建配置

拉取配置

配置热更新

@RefreshScope

@ConfigurationProperties

配置共享

配置优先级

六、Feign远程调用

Feign使用

自定义配置

性能优化

最佳实践

继承方式

抽取方式

七、Gateway网关

入门使用

流程图

断言工厂

过滤器工厂

全局过滤器

过滤器顺序

跨域问题

八、RabbitMQ

同步异步通讯

MQ消息队列

入门案例

publisher实现

consumer实现

SpringAMQP

BasicQueue(普通队列)

WorkQueue(工作队列)

发布/订阅

Fanout

Direct

Topic

消息转换器


一、认识微服务

单体架构

单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署。

SpringCloud学习笔记_第1张图片

优点:架构简单,部署成本低

缺点:耦合度高(维护困难、升级困难)

分布式架构

分布式架构:根据业务功能对系统做拆分,每个业务功能模块作为独立项目开发,称为一个服务。

SpringCloud学习笔记_第2张图片

优点:降低服务耦合,有利于服务升级和拓展

缺点:服务调用关系错综复杂

分布式架构虽然降低了服务耦合,但是服务拆分时也有很多问题需要思考

  • 服务拆分的粒度如何界定?
  • 服务之间如何调用?
  • 服务的调用关系如何管理?

人们需要制定一套行之有效的标准来约束分布式架构。

微服务

微服务的架构特征:

  • 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责
  • 自治:团队独立、技术独立、数据独立,独立部署和交付
  • 面向服务:服务提供统一标准的接口,与语言和技术无关
  • 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题

SpringCloud学习笔记_第3张图片

微服务的上述特性其实是在给分布式架构制定一个标准,进一步降低服务之间的耦合度,提供服务的独立性和灵活性。做到高内聚,低耦合。

因此,可以认为微服务是一种经过良好架构设计的分布式架构方案 。

其中在 Java 领域最引人注目的就是 SpringCloud 提供的方案了。

SpringCloud

SpringCloud 是目前国内使用最广泛的微服务框架。官网地址:Spring Cloud。

SpringCloud 集成了各种微服务功能组件,并基于 SpringBoot 实现了这些组件的自动装配,从而提供了良好的开箱即用体验。

其中常见的组件包括:

SpringCloud学习笔记_第4张图片

内容知识

需要学习的微服务知识内容

SpringCloud学习笔记_第5张图片

技术栈

SpringCloud学习笔记_第6张图片

技术栈对比

SpringCloud学习笔记_第7张图片

服务拆分

服务拆分注意事项

单一职责:不同微服务,不要重复开发相同业务

数据独立:不要访问其它微服务的数据库

面向服务:将自己的业务暴露为接口,供其它微服务调用

SpringCloud学习笔记_第8张图片

cloud-demo:父工程,管理依赖

  • order-service:订单微服务,负责订单相关业务
  • user-service:用户微服务,负责用户相关业务

要求:

  • 订单微服务和用户微服务都必须有各自的数据库,相互独立
  • 订单服务和用户服务都对外暴露 Restful 的接口
  • 订单服务如果需要查询用户信息,只能调用用户服务的 Restful 接口,不能查询用户数据库

微服务项目下,打开 idea 中的 Service,可以很方便的启动。

SpringCloud学习笔记_第9张图片

启动完成后,访问 http://localhost:8080/order/101

SpringCloud学习笔记_第10张图片

远程调用

正如上面的服务拆分要求中所提到,

订单服务如果需要查询用户信息, 只能调用用户服务的 Restful 接口,不能查询用户数据库

因此我们需要知道 Java 如何去发送 http 请求,Spring 提供了一个 RestTemplate 工具,只需要把它创建出来即可。(即注入 Bean)

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {

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

    /**
     * 创建RestTemplate并注入spring容器
     * @return
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

发送请求,自动序列化为 Java 对象。

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RestTemplate restTemplate;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 2.查询用户
        String url = "http://localhost:8081/user/" + order.getUserId();
        User user = restTemplate.getForObject(url, User.class);
        order.setUser(user);
        // 4.返回
        return order;
    }
}

启动完成后,访问:

http://localhost:8080/order/101

在上面代码的 url 中,我们可以发现调用服务的地址采用硬编码,这在后续的开发中肯定是不理想的,这就需要服务注册中心(Eureka)来帮我们解决这个事情。

二、Eureka注册中心

最广为人知的注册中心就是 Eureka,其结构如下:

SpringCloud学习笔记_第11张图片

order-service 如何得知 user-service 实例地址?

  • user-service 服务实例启动后,将自己的信息注册到 eureka-server(Eureka服务端),叫做服务注册
  • eureka-server 保存服务名称到服务实例地址列表的映射关系
  • order-service 根据服务名称,拉取实例地址列表,这个叫服务发现或服务拉取

order-service 如何从多个 user-service 实例中选择具体的实例?

order-service从实例列表中利用负载均衡算法选中一个实例地址,向该实例地址发起远程调用

order-service 如何得知某个 user-service 实例是否依然健康,是不是已经宕机?

  • user-service 会每隔一段时间(默认30秒)向 eureka-server 发起请求,报告自己状态,称为心跳
  • 当超过一定时间没有发送心跳时,eureka-server 会认为微服务实例故障,将该实例从服务列表中剔除
  • order-service 拉取服务时,就能将故障实例排除了

接下来我们动手实践的步骤包括:

SpringCloud学习笔记_第12张图片

搭建注册中心

搭建 eureka-server

引入 SpringCloud 为 eureka 提供的 starter 依赖,注意这里是用 server


    org.springframework.cloud
    spring-cloud-starter-netflix-eureka-server

编写启动类

注意要添加一个 @EnableEurekaServer 注解,开启 eureka 的注册中心功能

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

编写配置文件

编写一个 application.yml 文件,内容如下:

server:
  port: 10086
spring:
  application:
    name: eurekaserver
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

其中 default-zone 是因为前面配置类开启了注册中心所需要配置的 eureka 的地址信息,因为 eureka 本身也是一个微服务,这里也要将自己注册进来,当后面 eureka 集群时,这里就可以填写多个,使用 “,” 隔开。

启动完成后,访问 http://localhost:10086/

SpringCloud学习笔记_第13张图片

服务注册

将 user-service、order-service 都注册到 eureka

引入 SpringCloud 为 eureka 提供的 starter 依赖,注意这里是用 client


    org.springframework.cloud
    spring-cloud-starter-netflix-eureka-client

在启动类上添加注解:@EnableEurekaClient

在 application.yml 文件,添加下面的配置:

spring:
  application:
      #name:orderservice
    name: userservice
eureka:
  client:
    service-url: 
      defaultZone: http:127.0.0.1:10086/eureka

3个项目启动后,访问 http://localhost:10086/

SpringCloud学习笔记_第14张图片

这里另外再补充个小技巧,我们可以通过 idea 的多实例启动,来查看 Eureka 的集群效果。

SpringCloud学习笔记_第15张图片

4个项目启动后,访问 http://localhost:10086/

SpringCloud学习笔记_第16张图片

服务拉取

在 order-service 中完成服务拉取,然后通过负载均衡挑选一个服务,实现远程调用

下面我们让 order-service 向 eureka-server 拉取 user-service 的信息,实现服务发现。

首先给 RestTemplate 这个 Bean 添加一个 @LoadBalanced 注解,用于开启负载均衡。(后面会讲)

    /**
     * 创建RestTemplate并注入spring容器
     * @return
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

修改 OrderService 访问的url路径,用服务名代替ip、端口:

SpringCloud学习笔记_第17张图片

spring 会自动帮助我们从 eureka-server 中,根据 userservice 这个服务名称,获取实例列表后去完成负载均衡。

三、Ribbon负载均衡

我们添加了 @LoadBalanced 注解,即可实现负载均衡功能,这是什么原理呢?

SpringCloud 底层提供了一个名为 Ribbon 的组件,来实现负载均衡功能。

SpringCloud学习笔记_第18张图片

源码跟踪

为什么我们只输入了 service 名称就可以访问了呢?为什么不需要获取ip和端口,这显然有人帮我们根据 service 名称,获取到了服务实例的ip和端口。它就是LoadBalancerInterceptor,这个类会在对 RestTemplate 的请求进行拦截,然后从 Eureka 根据服务 id 获取服务列表,随后利用负载均衡算法得到真实的服务地址信息,替换服务 id。

我们进行源码跟踪:

SpringCloud学习笔记_第19张图片

这里的 intercept() 方法,拦截了用户的 HttpRequest 请求,然后做了几件事:

  • request.getURI():获取请求uri,即 http://user-service/user/8
  • originalUri.getHost():获取uri路径的主机名,其实就是服务id user-service
  • this.loadBalancer.execute():处理服务id,和用户请求

这里的 this.loadBalancer 是 LoadBalancerClient 类型

继续跟入 execute() 方法:

SpringCloud学习笔记_第20张图片

  • getLoadBalancer(serviceId):根据服务id获取 ILoadBalancer,而 ILoadBalancer 会拿着服务 id 去 eureka 中获取服务列表。
  • getServer(loadBalancer):利用内置的负载均衡算法,从服务列表中选择一个。在图中可以看到获取了8082端口的服务

可以看到获取服务时,通过一个 getServer() 方法来做负载均衡:

SpringCloud学习笔记_第21张图片

我们继续跟入:

SpringCloud学习笔记_第22张图片

继续跟踪源码 chooseServer() 方法,发现这么一段代码:

SpringCloud学习笔记_第23张图片

我们看看这个 rule 是谁:

SpringCloud学习笔记_第24张图片

这里的 rule 默认值是一个 RoundRobinRule ,看类的介绍:

SpringCloud学习笔记_第25张图片

负载均衡默认使用了轮训算法,当然我们也可以自定义。

流程总结

SpringCloud Ribbon 底层采用了一个拦截器,拦截了 RestTemplate 发出的请求,对地址做了修改。

基本流程如下:

  • 拦截我们的 RestTemplate 请求 http://userservice/user/1
  • RibbonLoadBalancerClient 会从请求url中获取服务名称,也就是 user-service
  • DynamicServerListLoadBalancer 根据 user-service 到 eureka 拉取服务列表
  • eureka 返回列表,localhost:8081、localhost:8082
  • IRule 利用内置负载均衡规则,从列表中选择一个,例如 localhost:8081
  • RibbonLoadBalancerClient 修改请求地址,用 localhost:8081 替代 userservice,得到 http://localhost:8081/user/1,发起真实请求

SpringCloud学习笔记_第26张图片

负载均衡策略

负载均衡的规则都定义在 IRule 接口中,而 IRule 有很多不同的实现类:

SpringCloud学习笔记_第27张图片

不同规则的含义如下:

内置负载均衡规则类 规则描述
RoundRobinRule 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。
AvailabilityFilteringRule 对以下两种服务器进行忽略:(1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule 规则的客户端也会将其忽略。并发连接数的上限,可以由客户端设置。
WeightedResponseTimeRule 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。
ZoneAvoidanceRule 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。
BestAvailableRule 忽略那些短路的服务器,并选择并发数较低的服务器。
RandomRule 随机选择一个可用的服务器。
RetryRule 重试机制的选择逻辑

 

默认的实现就是 ZoneAvoidanceRule是一种轮询方案

自定义策略

通过定义 IRule 实现可以修改负载均衡规则,有两种方式:

1 代码方式在 order-service 中的 OrderApplication 类中,定义一个新的 IRule:

SpringCloud学习笔记_第28张图片

2 配置文件方式:在 order-service 的 application.yml 文件中,添加新的配置也可以修改规则:

userservice: # 给需要调用的微服务配置负载均衡规则,orderservice服务去调用userservice服务
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则

 注意:一般用默认的负载均衡规则,不做修改。

饥饿加载

当我们启动 orderservice,第一次访问时,时间消耗会大很多,这是因为 Ribbon 懒加载的机制。

Ribbon 默认是采用懒加载,即第一次访问时才会去创建 LoadBalanceClient,拉取集群地址,所以请求时间会很长。

而饥饿加载则会在项目启动时创建 LoadBalanceClient,降低第一次访问的耗时,通过下面配置开启饥饿加载:

ribbon:
  eager-load:
    enabled: true
    clients: userservice # 项目启动时直接去拉取userservice的集群,多个用","隔开

四、Nacos注册中心

SpringCloudAlibaba 推出了一个名为 Nacos 的注册中心,在国外也有大量的使用。

SpringCloud学习笔记_第29张图片

解压启动 Nacos访问:http://localhost:8848/nacos/

SpringCloud学习笔记_第30张图片

服务注册

这里上来就直接服务注册,很多东西可能有疑惑,其实 Nacos 本身就是一个 SprintBoot 项目,这点你从启动的控制台打印就可以看出来,所以就不再需要去额外搭建一个像 Eureka 的注册中心。

引入依赖

在 cloud-demo 父工程中引入 SpringCloudAlibaba 的依赖:


    com.alibaba.cloud
    spring-cloud-alibaba-dependencies
    2.2.6.RELEASE
    pom
    import

然后在 user-service 和 order-service 中的pom文件中引入 nacos-discovery 依赖:


    com.alibaba.cloud
    spring-cloud-starter-alibaba-nacos-discovery

配置nacos地址

在 user-service 和 order-service 的 application.yml 中添加 nacos 地址:

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

项目重新启动后,可以看到三个服务都被注册进了 Nacos

SpringCloud学习笔记_第31张图片

浏览器访问:http://localhost:8080/order/101,正常访问,同时负载均衡也正常。

分级存储模

一个服务可以有多个实例,例如我们的 user-service,可以有:

  • 127.0.0.1:8081
  • 127.0.0.1:8082
  • 127.0.0.1:8083

假如这些实例分布于全国各地的不同机房,例如:

  • 127.0.0.1:8081,在上海机房
  • 127.0.0.1:8082,在上海机房
  • 127.0.0.1:8083,在杭州机房

Nacos就将同一机房内的实例,划分为一个集群

SpringCloud学习笔记_第32张图片

微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。当本集群内不可用时,才访问其它集群。例如:杭州机房内的 order-service 应该优先访问同机房的 user-service。

SpringCloud学习笔记_第33张图片

配置集群

接下来我们给 user-service 配置集群

修改 user-service 的 application.yml 文件,添加集群配置:

spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ # 集群名称 HZ杭州

重启两个 user-service 实例后,我们再去启动一个上海集群的实例。

-Dserver.port=8083 -Dspring.cloud.nacos.discovery.cluster-name=SH

SpringCloud学习笔记_第34张图片

查看 nacos 控制台:

SpringCloud学习笔记_第35张图片

NacosRule

Ribbon的默认实现 ZoneAvoidanceRule 并不能实现根据同集群优先来实现负载均衡,我们把规则改成 NacosRule 即可。我们是用 orderservice 调用 userservice,所以在 orderservice 配置规则。

@Bean
public IRule iRule(){
    //默认为轮询规则,这里自定义为随机规则
    return new NacosRule();
}

另外,你同样可以使用配置的形式来完成,具体参考上面的 Ribbon 栏目。

userservice:
  ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule #负载均衡规则 

然后,再对 orderservice 配置集群。

spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ # 集群名称

现在我启动了四个服务,分别是:

  • orderservice - HZ
  • userservice - HZ
  • userservice1 - HZ
  • userservice2 - SH

访问地址:http://localhost:8080/order/101

在访问中我们发现,只有同在一个 HZ 集群下的 userservice、userservice1 会被调用,并且是随机的。

我们试着把 userservice、userservice2 停掉。依旧可以访问。

在 userservice3 控制台可以看到发出了一串的警告,因为 orderservice 本身是在 HZ 集群的,这波 HZ 集群没有了 userservice,就会去别的集群找。

SpringCloud学习笔记_第36张图片

权重配置

实际部署中会出现这样的场景:

服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。但默认情况下 NacosRule 是同集群内随机挑选,不会考虑机器的性能问题。

因此,Nacos 提供了权重配置来控制访问频率,0~1 之间,权重越大则访问频率越高,权重修改为 0,则该实例永远不会被访问。

在 Nacos 控制台,找到 user-service 的实例列表,点击编辑,即可修改权重。

SpringCloud学习笔记_第37张图片

在弹出的编辑窗口,修改权重

SpringCloud学习笔记_第38张图片

另外,在服务升级的时候,有一种较好的方案:我们也可以通过调整权重来进行平滑升级,例如:先把 userservice 权重调节为 0,让用户先流向 userservice2、userservice3,升级 userservice后,再把权重从 0 调到 0.1,让一部分用户先体验,用户体验稳定后就可以往上调权重啦。

环境隔离

Nacos 提供了 namespace 来实现环境隔离功能。

  • Nacos 中可以有多个 namespace
  • namespace 下可以有 group、service 等
  • 不同 namespace 之间相互隔离,例如不同 namespace 的服务互相不可见

SpringCloud学习笔记_第39张图片

创建namespace

默认情况下,所有 service、data、group 都在同一个 namespace,名为 public(保留空间):

SpringCloud学习笔记_第40张图片

我们可以点击页面新增按钮,添加一个 namespace:

SpringCloud学习笔记_第41张图片

然后,填写表单:

SpringCloud学习笔记_第42张图片

就能在页面看到一个新的 namespace:

SpringCloud学习笔记_第43张图片

配置namespace

给微服务配置 namespace 只能通过修改配置来实现。

例如,修改 order-service 的 application.yml 文件:

spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ
        namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 # 命名空间ID

重启 order-service 后,访问控制台。

public

SpringCloud学习笔记_第44张图片

dev

SpringCloud学习笔记_第45张图片

此时访问 order-service,因为 namespace 不同,会导致找不到 userservice,控制台会报错:

临时实例

Nacos 的服务实例分为两种类型:

  • 临时实例:如果实例宕机超过一定时间,会从服务列表剔除,默认的类型
  • 非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例。

配置一个服务实例为永久实例:

spring:
  cloud:
    nacos:
      discovery:
        ephemeral: false # 设置为非临时实例

另外,Nacos 集群默认采用AP方式(可用性),当集群中存在非临时实例时,采用CP模式(一致性);而 Eureka 采用AP方式,不可切换。(这里说的是 CAP 原理,后面会写到)

五、Nacos配置中心

Nacos除了可以做注册中心,同样可以做配置管理来使用。

当微服务部署的实例越来越多,达到数十、数百时,逐个修改微服务配置就会让人抓狂,而且很容易出错。我们需要一种统一配置管理方案,可以集中管理所有实例的配置。

SpringCloud学习笔记_第46张图片

Nacos 一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新。

创建配置

在 Nacos 控制面板中添加配置文件

SpringCloud学习笔记_第47张图片

然后在弹出的表单中,填写配置信息:

SpringCloud学习笔记_第48张图片

注意:项目的核心配置,需要热更新的配置才有放到 nacos 管理的必要。基本不会变更的一些配置(例如数据库连接)还是保存在微服务本地比较好。

拉取配置

首先我们需要了解 Nacos 读取配置文件的环节是在哪一步,在没加入 Nacos 配置之前,获取配置是这样:

SpringCloud学习笔记_第49张图片

加入 Nacos 配置,它的读取是在 application.yml 之前的:

SpringCloud学习笔记_第50张图片

这时候如果把 nacos 地址放在 application.yml 中,显然是不合适的,Nacos 就无法根据地址去获取配置了。

因此,nacos 地址必须放在优先级最高的 bootstrap.yml 文件。

SpringCloud学习笔记_第51张图片

引入 nacos-config 依赖

首先,在 user-service 服务中,引入 nacos-config 的客户端依赖:



    com.alibaba.cloud
    spring-cloud-starter-alibaba-nacos-config

添加 bootstrap.yml

然后,在 user-service 中添加一个 bootstrap.yml 文件,内容如下:

spring:
  application:
    name: userservice # 服务名称
  profiles:
    active: dev #开发环境,这里是dev 
  cloud:
    nacos:
      server-addr: localhost:8848 # Nacos地址
      config:
        file-extension: yaml # 文件后缀名

根据 spring.cloud.nacos.server-addr 获取 nacos地址,再根据${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}作为文件id,来读取配置。

在这个例子例中,就是去读取 userservice-dev.yaml

SpringCloud学习笔记_第52张图片

使用代码来验证是否拉取成功

在 user-service 中的 UserController 中添加业务逻辑,读取 pattern.dateformat 配置并使用:

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

@GetMapping("now")
public String now(){
    //格式化时间
    return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}

启动服务后,访问:http://localhost:8081/user/now

配置热更新

我们最终的目的,是修改 nacos 中的配置后,微服务中无需重启即可让配置生效,也就是配置热更新

有两种方式:1. 用 @value 读取配置时,搭配 @RefreshScope;2. 直接用 @ConfigurationProperties 读取配置

@RefreshScope

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

SpringCloud学习笔记_第53张图片

@ConfigurationProperties

方式二:使用 @ConfigurationProperties 注解读取配置文件,就不需要加 @RefreshScope 注解。

在 user-service 服务中,添加一个 PatternProperties 类,读取 patterrn.dateformat 属性

@Data
@Component
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
    private String dateformat;
    private String envSharedValue;
    private String name;
}

 

    @Autowired
    private PatternProperties properties;

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

配置共享

其实在服务启动时,nacos 会读取多个配置文件,例如:

  • [spring.application.name]-[spring.profiles.active].yaml,例如:userservice-dev.yaml
  • [spring.application.name].yaml,例如:userservice.yaml

这里的 [spring.application.name].yaml 不包含环境,因此可以被多个环境共享

添加一个环境共享配置

我们在 nacos 中添加一个 userservice.yaml 文件:

SpringCloud学习笔记_第54张图片

在 user-service 中读取共享配置

在 user-service 服务中,修改 PatternProperties 类,读取新添加的属性:

在 user-service 服务中,修改 UserController,添加一个方法:

SpringCloud学习笔记_第55张图片

运行两个 UserApplication,使用不同的profile

修改 UserApplication2 这个启动项,改变其profile值:

SpringCloud学习笔记_第56张图片

SpringCloud学习笔记_第57张图片

这样,UserApplication(8081) 使用的 profile 是 dev,UserApplication2(8082) 使用的 profile 是test

启动 UserApplication 和 UserApplication2

访问地址:http://localhost:8081/user/prop,结果:

SpringCloud学习笔记_第58张图片

访问地址:http://localhost:8082/user/prop,结果:

SpringCloud学习笔记_第59张图片

可以看出来,不管是 dev,还是 test 环境,都读取到了 envSharedValue 这个属性的值。

上面的都是同一个微服务下,那么不同微服务之间可以环境共享吗?

通过下面的两种方式来指定:

  • extension-configs
  • shared-configs
spring: 
  cloud:
    nacos:
      config:
        file-extension: yaml # 文件后缀名
        extends-configs: # 多微服务间共享的配置列表
          - dataId: common.yaml # 要共享的配置文件id
spring: 
  cloud:
    nacos:
      config:
        file-extension: yaml # 文件后缀名
        shared-configs: # 多微服务间共享的配置列表
          - dataId: common.yaml # 要共享的配置文件id

 

配置优先级

当 nacos、服务本地同时出现相同属性时,优先级有高低之分。

SpringCloud学习笔记_第60张图片

更细致的配置

SpringCloud学习笔记_第61张图片

六、Feign远程调用

我们以前利用 RestTemplate 发起远程调用的代码:

  • 代码可读性差,编程体验不统一
  • 参数复杂URL难以维护

Feign 是一个声明式的 http 客户端,官方地址:GitHub - OpenFeign/feign: Feign makes writing java http clients easier

其作用就是帮助我们优雅的实现 http 请求的发送,解决上面提到的问题。

SpringCloud学习笔记_第62张图片

Feign使用

引入依赖

我们在 order-service 引入 feign 依赖:

@FeignClient("userservice")
public interface UserClient {

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

}

    org.springframework.cloud
    spring-cloud-starter-openfeign

添加注解

在 order-service 启动类添加注解开启 Feign

SpringCloud学习笔记_第63张图片

请求接口

在 order-service 中新建一个接口,内容如下

@FeignClient("userservice")
public interface UserClient {

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

}

@FeignClient("userservice"):其中参数填写的是微服务名

@GetMapping("/user/{id}"):其中参数填写的是请求路径

这个客户端主要是基于 SpringMVC 的注解 @GetMapping 来声明远程调用的信息

Feign 可以帮助我们发送 http 请求,无需自己使用 RestTemplate 来发送了。

    @Autowired
    private UserClient userClient;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 2.查询用户
        User user = userClient.findById(order.getUserId());
        // 3.将用户信息封装进订单
        order.setUser(user);
        // 4.返回
        return order;
    }

自定义配置

Feign 可以支持很多的自定义配置,如下表所示:

类型 作用 说明
feign.Logger.Level 修改日志级别 包含四种不同的级别:NONE、BASIC、HEADERS、FULL
feign.codec.Decoder 响应结果的解析器 http远程调用的结果做解析,例如解析json字符串为java对象
feign.codec.Encoder 请求参数编码 将请求参数编码,便于通过http请求发送
feign.Contract 支持的注解格式 默认是SpringMVC的注解
feign.Retryer 失败重试机制 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试

一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的 @Bean 覆盖默认 Bean 即可。下面以日志为例来演示如何自定义配置。

基于配置文件修改 feign 的日志级别可以针对单个服务:

feign:  
  client:
    config: 
      userservice: # 针对某个微服务的配置
        loggerLevel: FULL #  日志级别 

也可以针对所有服务:

feign:  
  client:
    config: 
      default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
        loggerLevel: FULL #  日志级别 

而日志的级别分为四种:

  • NONE:不记录任何日志信息,这是默认值。
  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据

也可以基于 Java 代码来修改日志级别,先声明一个类,然后声明一个 Logger.Level 的对象

public class DefaultFeignConfiguration  {
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.BASIC; // 日志级别为BASIC
    }
}

如果要全局生效,将其放到启动类的 @EnableFeignClients 这个注解中:

@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class) 

如果是局部生效,则把它放到对应的 @FeignClient 这个注解中:

@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration .class) 

性能优化

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

  • URLConnection:默认实现,不支持连接池
  • Apache HttpClient :支持连接池
  • OKHttp:支持连接池

因此提高 Feign 性能的主要手段就是使用连接池代替默认的 URLConnection

另外,日志级别应该尽量用 basic/none,可以有效提高性能。

这里我们用 Apache 的HttpClient来演示连接池。

在 order-service 的 pom 文件中引入 HttpClient 依赖



    io.github.openfeign
    feign-httpclient

配置连接池

在 order-service 的 application.yml 中添加配置

feign:
  client:
    config:
      default: # default全局的配置
        loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息
  httpclient:
    enabled: true # 开启feign对HttpClient的支持
    max-connections: 200 # 最大的连接数
    max-connections-per-route: 50 # 每个路径的最大连接数

在 FeignClientFactoryBean 中的 loadBalance 方法中打断点

SpringCloud学习笔记_第64张图片

Debug 方式启动 order-service 服务,可以看到这里的 client,底层就是 HttpClient

SpringCloud学习笔记_第65张图片

最佳实践

继承方式

一样的代码可以通过继承来共享:

1)定义一个 API 接口,利用定义方法,并基于 SpringMVC 注解做声明

2)Feign 客户端、Controller 都集成该接口

SpringCloud学习笔记_第66张图片

优点

  • 简单
  • 实现了代码共享

缺点

  • 服务提供方、服务消费方紧耦合
  • 参数列表中的注解映射并不会继承,因此 Controller 中必须再次声明方法、参数列表、注解

抽取方式

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

例如:将 UserClient、User、Feign 的默认配置都抽取到一个 feign-api 包中,所有微服务引用该依赖包,即可直接使用。

SpringCloud学习笔记_第67张图片

接下来我们就用该方法在代码中实现

首先创建一个 module,命名为 feign-api

SpringCloud学习笔记_第68张图片

在 feign-api 中然后引入依赖


    org.springframework.cloud
    spring-cloud-starter-openfeign

order-service中的 UserClient、User 都复制到 feign-api 项目中

SpringCloud学习笔记_第69张图片

在order-service中使用 feign-api

首先,删除 order-service 中的 UserClient、User

在 order-service 中引入 feign-api


    com.xn2001.feign
    feign-api
    1.0

修改注解

当定义的 FeignClient 不在 SpringBootApplication 的扫描包范围下时,这些 FeignClient 就不能使用。

修改 order-service 启动类上的 @EnableFeignClients 注解

@EnableFeignClients(basePackages = "com.xn2001.feign.clients")

七、Gateway网关

Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

Gateway 网关是我们服务的守门神,所有微服务的统一入口。

网关的核心功能特性

  • 请求路由
  • 权限控制
  • 限流

SpringCloud学习笔记_第70张图片

权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。

路由和负载均衡:一切请求都必须先经过 gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。

限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。

在 SpringCloud 中网关的实现包括两种:

  • gateway
  • zuul

Zuul 是基于 Servlet 实现,属于阻塞式编程。而 Spring Cloud Gateway 则是基于 Spring5 中提供的WebFlux,属于响应式编程的实现,具备更好的性能。

入门使用

  1. 创建 SpringBoot 工程 gateway,引入网关依赖
  2. 编写启动类
  3. 编写基础配置和路由规则
  4. 启动网关服务进行测试


    org.springframework.cloud
    spring-cloud-starter-gateway



    com.alibaba.cloud
    spring-cloud-starter-alibaba-nacos-discovery

创建 application.yml 文件,内容如下:

server:
  port: 10010 # 网关端口
spring:
  application:
    name: gateway # 服务名称
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos地址
    gateway:
      routes: # 网关路由配置
        - id: user-service # 路由id,自定义,只要唯一即可
          # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
          uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求

我们将符合Path 规则的一切请求,都代理到 uri参数指定的地址。

上面的例子中,我们将 /user/** 开头的请求,代理到 lb://userservice,其中 lb 是负载均衡(LoadBalance),根据服务名拉取服务列表,实现负载均衡。

重启网关,访问 http://localhost:10010/user/1 时,符合 /user/** 规则,请求转发到 uri:http://userservice/user/1

SpringCloud学习笔记_第71张图片

多个 predicates 的话,要同时满足规则,下文有例子。

流程图

SpringCloud学习笔记_第72张图片

路由配置包括:

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

断言工厂

我们在配置文件中写的断言规则只是字符串,这些字符串会被 Predicate Factory 读取并处理,转变为路由判断的条件。

例如 Path=/user/** 是按照路径匹配,这个规则是由

org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory 类来处理的,像这样的断言工厂在 Spring Cloud Gateway 还有十几个

名称 说明 示例
After 是某个时间点后的请求 - After=2037-01-20T17:42:47.789-07:00[America/Denver]
Before 是某个时间点之前的请求 - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
Between 是某两个时间点之前的请求 - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]
Cookie 请求必须包含某些cookie - Cookie=chocolate, ch.p
Header 请求必须包含某些header - Header=X-Request-Id, \d+
Host 请求必须是访问某个host(域名) - Host=**.somehost.org**.anotherhost.org
Method 请求方式必须是指定方式 - Method=GET,POST
Path 请求路径必须符合指定规则 - Path=/red/{segment},/blue/**
Query 请求参数必须包含指定参数 - Query=name, Jack或者- Query=name
RemoteAddr 请求者的ip必须是指定范围 - RemoteAddr=192.168.1.1/24
Weight 权重处理
官方文档: Spring Cloud Gateway

一般的,我们只需要掌握 Path,加上官方文档的例子,就可以应对各种工作场景了。

predicates:
  - Path=/order/**
  - After=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]

像这样的规则,现在是 2021年8月22日01:32:42,很明显 After 条件不满足,可以不会转发,路由不起作用。

过滤器工厂

GatewayFilter 是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理。

SpringCloud学习笔记_第73张图片

Spring提供了31种不同的路由过滤器工厂。

官方文档: Spring Cloud Gateway
名称 说明
AddRequestHeader 给当前请求添加一个请求头
RemoveRequestHeader 移除请求中的一个请求头
AddResponseHeader 给响应结果中添加一个响应头
RemoveResponseHeader 从响应结果中移除有一个响应头
RequestRateLimiter 限制请求的流量

下面我们以 AddRequestHeader 为例:

SpringCloud学习笔记_第74张图片

需求:给所有进入 userservice 的请求添加一个请求头:sign=xn2001.com is eternal

只需要修改 gateway 服务的 application.yml文件,添加路由过滤即可。

spring:
  cloud:
    gateway:
      routes: # 网关路由配置
        - id: user-service # 路由id,自定义,只要唯一即可
          # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
          uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
          filters:
            - AddRequestHeader=sign, xn2001.com is eternal # 添加请求头

如何验证,我们修改 userservice 中的一个接口

@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id, @RequestHeader(value = "sign", required = false) String sign) {
    log.warn(sign);
    return userService.queryById(id);
}

重启两个服务,访问:http://localhost:10010/user/1

可以看到控制台打印出了这个请求头

SpringCloud学习笔记_第75张图片

当然,Gateway 也是有全局过滤器的,如果要对所有的路由都生效,则可以将过滤器工厂写到 default-filters 下:

spring:
  cloud:
    gateway:
      default-filters:
        - AddRequestHeader=sign, xn2001.com is eternal # 添加请求头

全局过滤器

上面介绍的过滤器工厂,网关提供了 31 种,但每一种过滤器的作用都是固定的。如果我们希望拦截请求,做自己的业务逻辑则没办法实现

全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与 GatewayFilter 的作用一样。区别在于 GlobalFilter 的逻辑可以写代码来自定义规则;而 GatewayFilter 通过配置定义,处理逻辑是固定的。

需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件

  • 参数中是否有 authorization
  • authorization 参数值是否为 admin

如果同时满足则放行,否则拦截。

@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {

    // 测试:http://localhost:10010/order/101?authorization=admin
    @Override
    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取第一个 authorization 参数
        String authorization = exchange.getRequest().getQueryParams().getFirst("authorization");
        if ("admin".equals(authorization)){
            // 放行
            return chain.filter(exchange);
        }
        // 设置拦截状态码信息
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        // 设置拦截
        return exchange.getResponse().setComplete();
    }

    // 设置过滤器优先级,值越低优先级越高
    // 也可以使用 @Order 注解
    @Override
    public int getOrder() {
        return 0;
    }
}

过滤器顺序

请求进入网关会碰到三类过滤器:DefaultFilter、当前路由的过滤器、GlobalFilter;

请求路由后,会将三者合并到一个过滤器链(集合)中,排序后依次执行每个过滤器.

SpringCloud学习笔记_第76张图片

排序的规则是什么呢?

  • 每一个过滤器都必须指定一个 int 类型的 order 值,order 值越小,优先级越高,执行顺序越靠前
  • GlobalFilter 通过实现 Ordered 接口,或者使用 @Order 注解来指定 order 值,由我们自己指定。
  • 路由过滤器和 defaultFilter 的 order 由 Spring 指定,默认是按照声明顺序从1递增。
  • 当过滤器的 order 值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter 的顺序执行。

跨域问题

不了解跨域问题的同学可以百度了解一下;在 Gateway 网关中解决跨域问题还是比较方便的。

spring:
  cloud:
    gateway:
      globalcors: # 全局的跨域处理
        add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
        corsConfigurations:
          '[/**]':
            allowedOrigins: # 允许哪些网站的跨域请求 allowedOrigins: “*” 允许所有网站
              - "http://localhost:8090"
            allowedMethods: # 允许的跨域ajax的请求方式
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*" # 允许在请求中携带的头信息
            allowCredentials: true # 是否允许携带cookie
            maxAge: 360000 # 这次跨域检测的有效期

八、RabbitMQ

同步异步通讯

微服务间通讯有同步和异步两种方式

同步通讯:就像打电话,需要实时响应。

异步通讯:就像发邮件,不需要马上回复。

SpringCloud学习笔记_第77张图片

两种方式各有优劣,打电话可以立即得到响应,但是你却不能跟多个人同时通话。发送邮件可以同时与多个人收发邮件,但是往往响应会有延迟。

我们之前学习的 Feign 调用就属于同步方式,虽然调用可以实时得到结果,但存在下面的问题:

SpringCloud学习笔记_第78张图片

同步调用的优点:

  • 时效性较强,可以立即得到结果

同步调用的缺点:

  • 耦合度高
  • 性能和吞吐能力下降
  • 有额外的资源消耗
  • 有级联失败问题

异步调用则可以避免上述问题,我们以购买商品为例,用户支付后需要调用订单服务完成订单状态修改,调用物流服务,从仓库分配响应的库存并准备发货。在事件模式中,支付服务是事件发布者(publisher),在支付完成后只需要发布一个支付成功的事件(event),事件中带上订单id。订单服务和物流服务是事件订阅者(Consumer),订阅支付成功的事件,监听到事件后完成自己业务即可。

为了解除事件发布者与订阅者之间的耦合,两者并不是直接通信,而是有一个中间人(Broker)。发布者发布事件到Broker,不关心谁来订阅事件。订阅者从Broker订阅事件,不关心谁发来的消息。

SpringCloud学习笔记_第79张图片

Broker 是一个像数据总线一样的东西,所有的服务要接收数据和发送数据都发到这个总线上,这个总线就像协议一样,让服务间的通讯变得标准和可控。

SpringCloud学习笔记_第80张图片

异步调用好处:

  • 吞吐量提升:无需等待订阅者处理完成,响应更快速
  • 故障隔离:服务没有直接调用,不存在级联失败问题
  • 调用间没有阻塞,不会造成无效的资源占用
  • 耦合度极低,每个服务都可以灵活插拔,可替换
  • 流量削峰:不管发布事件的流量波动多大,都由 Broker 接收,订阅者可以按照自己的速度去处理事件

异步调用缺点:

  • 架构复杂了,业务没有明显的流程线,不好管理
  • 需要依赖于 Broker 的可靠、安全、性能

MQ消息队列

MQ,中文是消息队列(MessageQueue),字面来看就是存放消息的队列,也就是事件驱动架构中的 Broker

比较常见的 MQ 实现:

  • ActiveMQ
  • RabbitMQ
  • RocketMQ
  • Kafka

几种常见MQ的对比:

RabbitMQ ActiveMQ RocketMQ Kafka
公司/社区 Rabbit Apache 阿里 Apache
开发语言 Erlang Java Java Scala&Java
协议支持 AMQP、XMPP、SMTP、STOMP OpenWire、STOMP、REST、XMPP、AMQP 自定义协议 自定义协议
可用性 一般
单机吞吐量 一般 非常高
消息延迟 微秒级 毫秒级 毫秒级 毫秒以内
消息可靠性 一般 一般

以 RabbitMQ 为例,我们在 Centos7 虚拟机中使用 Docker 来安装

在线拉取镜像

docker pull rabbitmq:3-management

执行下面的命令来运行MQ容器

docker run \
 -e RABBITMQ_DEFAULT_USER=admin \
 -e RABBITMQ_DEFAULT_PASS=123456 \
 --name mq \
 --hostname mq1 \
 -p 15672:15672 \
 -p 5672:5672 \
 -d \
 rabbitmq:3-management

启动成功后访问地址:http://192.168.211.128:15672

RabbitMQ 中的一些角色

  • publisher:生产者
  • consumer:消费者
  • exchange:交换机,负责消息路由
  • queue:队列,存储消息
  • virtualHost:虚拟主机,隔离不同租户的 exchange、queue、消息的隔离

MQ 的基本结构

SpringCloud学习笔记_第81张图片

入门案例

RabbitMQ 官方提供了 5 个不同的 Demo 示例,对应了不同的消息模型。

SpringCloud学习笔记_第82张图片

Hello World 模型

SpringCloud学习笔记_第83张图片

官方的 HelloWorld 是基于最基础的消息队列模型来实现的,只包括三个角色:

  • publisher:消息发布者,将消息发送到队列queue
  • queue:消息队列,负责接受并缓存消息
  • consumer:订阅队列,处理队列中的消息

publisher实现

  • 建立连接
  • 创建 channel
  • 声明队列
  • 发送消息
  • 关闭连接和 channel
public class PublisherTest {
    @Test
    public void testSendMessage() throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("192.168.211.128");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("admin");
        factory.setPassword("123456");
        // 1.2.建立连接
        Connection connection = factory.newConnection();
        // 2.创建通道Channel
        Channel channel = connection.createChannel();
        // 3.创建队列
        String queueName = "simple.queue";
        channel.queueDeclare(queueName, false, false, false, null);
        // 4.发送消息
        String message = "Hello RabbitMQ!";
        channel.basicPublish("", queueName, null, message.getBytes());
        System.out.println("发送消息成功:[" + message + "]");
        // 5.关闭通道和连接
        channel.close();
        connection.close();
    }
}

consumer实现

  • 建立连接
  • 创建 channel
  • 声明队列
  • 订阅消息
public class ConsumerTest {
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("192.168.211.128");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("admin");
        factory.setPassword("123456");
        // 1.2.建立连接
        Connection connection = factory.newConnection();
        // 2.创建通道Channel
        Channel channel = connection.createChannel();
        // 3.创建队列
        String queueName = "simple.queue";
        channel.queueDeclare(queueName, false, false, false, null);
        // 4.订阅消息
        channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) {
                // 5.处理消息
                String message = new String(body);
                System.out.println("接收到消息:[" + message + "]");
            }
        });
        System.out.println("等待接收消息中");
    }
}

SpringAMQP

SpringAMQP 是基于 RabbitMQ 封装的一套模板,并且还利用 SpringBoot 对其实现了自动装配,使用起来非常方便。

SpringAMQP 的官方地址:Spring AMQP

SpringCloud学习笔记_第84张图片

SpringCloud学习笔记_第85张图片

SpringAMQP 提供了三个功能:

  • 自动声明队列、交换机及其绑定关系
  • 基于注解的监听器模式,异步接收消息
  • 封装了 RabbitTemplate 工具,用于发送消息


    org.springframework.boot
    spring-boot-starter-amqp

BasicQueue(普通队列)

首先配置 MQ地址,在 publisher、consumer 服务中的 application.yml 中添加配置

spring:
  rabbitmq:
    host: 192.168.xx.xx # 主机名
    port: 5672 # 端口
    virtual-host: / # 虚拟主机
    username: admin # 用户名
    password: 123456 # 密码

在 consumer 服务中添加监听队列

@Component
public class SpringRabbitListener {

    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg){
        System.out.println("消费者接收到了:"+msg);
    }
}

在 publisher 服务中添加发送消息的测试类

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {


    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendMessage2SimpleQueue(){
        String queueName = "simple.queue";
        String message = "hello world";
        rabbitTemplate.convertAndSend(queueName,message);
    }
}

WorkQueue(工作队列)

Work queues,也被称为(Task queues),任务模型。简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息

SpringCloud学习笔记_第86张图片

当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。

此时就可以使用 work 模型,多个消费者共同处理消息处理,速度就能大大提高了。

我们循环发送,模拟大量消息堆积现象,在 publisher 服务中的 SpringAmqpTest 类中添加一个测试方法:

/**
 * workQueue
 * 向队列中不停发送消息,模拟消息堆积。
 */
@Test
public void testWorkQueue() throws InterruptedException {
    // 队列名称
    String queueName = "simple.queue";
    // 消息
    String message = "hello, message_";
    for (int i = 0; i < 50; i++) {
        // 发送消息
        rabbitTemplate.convertAndSend(queueName, message + i);
        Thread.sleep(20);
    }
}

消息接收

要模拟多个消费者绑定同一个队列,我们在 consumer 服务的 RabbitMQListener 中添加2个新的方法:

    @RabbitListener(queues = "simple.queue")
    public void listenWorkQueue1(String msg) throws InterruptedException {
        System.out.println("消费者1接收到了:"+msg + ","+ LocalTime.now());
        Thread.sleep(20);
    }

    @RabbitListener(queues = "simple.queue")
    public void listenWorkQueue2(String msg) throws InterruptedException {
        System.err.println("消费者2接收到了:"+msg + ","+ LocalTime.now());
        Thread.sleep(200);
    }

启动 ConsumerApplication 后,在执行 publisher 服务中刚刚编写的发送测试方法 testWorkQueue

可以看到消费者1很快完成了自己的25条消息。消费者2却在缓慢的处理自己的25条消息。

也就是说消息是平均分配给每个消费者,并没有考虑到消费者的处理能力。这是因为 RabbitMQ 默认有一个消息预取机制,显然这不是我们想要的结果,我们需要的是能者多劳嘛,所以去限制每次只能取一条消息,可以解决这个问题。

在 spring 中有一个简单的配置,设置 prefetch 属性,我们修改 consumer 服务的 application.yml 文件,添加配置

spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息

Work 模型的使用:

  • 多个消费者绑定到一个队列,同一条消息只会被一个消费者处理
  • 通过设置 prefetch 来控制消费者预取的消息数量

发布/订阅

SpringCloud学习笔记_第87张图片

图中可以看到,在订阅模型中,多了一个 exchange 角色,而且过程略有变化

  • Publisher:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给 exchange(交换机)
  • Consumer:消费者,与以前一样,订阅队列,没有变化
  • Queue:消息队列也与以前一样,接收消息、缓存消息
  • Exchange:交换机,一方面,接收生产者发送的消息;另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于 Exchange 的类型。Exchange 有以下3种类型:

    • Fanout:广播,将消息交给所有绑定到交换机的队列
    • Direct:定向,把消息交给符合指定 routing key 的队列
    • Topic:通配符,把消息交给符合 routing pattern(路由模式) 的队列

Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与 Exchange 绑定,或者没有符合路由规则的队列,那么消息会丢失!

Fanout

Fanout,英文翻译是扇出,在 MQ 中我们也可以称为广播。

SpringCloud学习笔记_第88张图片

在广播模式下,消息发送流程是这样的:

  • 可以有多个队列
  • 每个队列都要绑定到 Exchange(交换机)
  • 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定
  • 交换机把消息发送给绑定过的所有队列
  • 订阅队列的消费者都能拿到消息

接下里我们用 SpringAMQP 来简单实现 FanoutExchange

  1. 在 consumer 服务中,利用代码声明队列、交换机,并将两者绑定
  2. 在 consumer 服务中,编写两个消费者方法,分别监听 fanout.queue1 和 fanout.queue2
  3. 在 publisher 中编写测试方法,向 fanout发送消息

声明队列和交换机

Spring 提供了一个接口 Exchange,来表示所有不同类型的交换机。

SpringCloud学习笔记_第89张图片

在 consumer 中创建一个类,声明队列、交换机、绑定对象 Binding

@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.queue2
    @Bean
    public Queue fanoutQueue2(){
        return new Queue("fanout.queue2");
    }

    // 绑定关系
    @Bean
    public Binding fanoutBinding1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
    }

    @Bean
    public Binding fanoutBinding2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
    }

}

SpringCloud学习笔记_第90张图片

通过这样 @Bean 的方式来申明确实比较麻烦,其实我们也是可以直接通过 @RabbitListener 注解来完成的,代码如下:

在 consumer 服务的 SpringRabbitListener 中添加三个方法,作为消费者

    @RabbitListener(queues = "fanout.queue1")
    public void listenFanoutQueue1(String msg){
        System.out.println("消费者1接收到了:"+msg);
    }

    @RabbitListener(queues = "fanout.queue2")
    public void listenFanoutQueue2(String msg){
        System.out.println("消费者2接收到了:"+msg);
    }

 在 publisher 服务的 SpringAmqpTest 类中添加测试方法

   @Test
    public void testSendFanoutExchange(){
        // 交换机
        String exchange = "itcast.fanout";
        String message = "hello message__";
        rabbitTemplate.convertAndSend(exchange,"",message);
    }

运行该方法,可以发现 fanout.queue1、fanout.queue2 都收到了交换机的消息。

总结一下:

交换机的作用是什么?

  • 接收 publisher 发送的消息
  • 将消息按照规则路由到与之绑定的队列
  • 不能缓存消息,路由失败,消息丢失
  • FanoutExchange 会将所有消息路由到每个绑定的队列

Direct

在 Fanout 模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到 DirectExchange

SpringCloud学习笔记_第91张图片

在 Direct 模型下:

  • 队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
  • 消息的发送方向 Exchange发送消息时,也必须指定消息的 RoutingKey
  • Exchange 不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routingkey 与消息的 Routing key完全一致,才会接收到消息

在 consumer 的 SpringRabbitListener 中添加两个消费者,同时基于注解来声明队列和交换机

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue1"),
            exchange = @Exchange(name = "itcast.direct"),
            key = {"red","blue"}
    ))
    public void listenDirectQueue1(String msg){
        System.out.println("消费者接收到了direct.queue1的消息:"+msg);
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue2"),
            exchange = @Exchange(name = "itcast.direct"),
            key = {"red","yellow"}
    ))
    public void listenDirectQueue2(String msg){
        System.out.println("消费者接收到了direct.queue2的消息:"+msg);
    }

 在 publisher 服务的 SpringAmqpTest 类中添加测试方法

    @Test
    public void testSendDirectExchangeA(){
        // 交换机
        String exchange = "itcast.direct";
        String message = "hello blue";
        rabbitTemplate.convertAndSend(exchange,"blue",message);
    }

    @Test
    public void testSendDirectExchangeB(){
        // 交换机
        String exchange = "itcast.direct";
        String message = "helloworld";
        rabbitTemplate.convertAndSend(exchange,"red",message);
    }

Topic

Topic 与 Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic 类型可以让队列在绑定Routing key 的时候使用通配符!

通配符规则:

#:匹配一个或多个词

*:只能匹配一个词

例如:

item.#:能够匹配item.spu.insert 或者 item.spu

item.*:只能匹配item.spu

SpringCloud学习笔记_第92张图片

  • Queue1:绑定的是 china.# ,因此凡是以 china. 开头的 routing key 都会被匹配到。包括 china.news 和 china.weather
  • Queue2:绑定的是 #.news ,因此凡是以 .news 结尾的 routing key 都会被匹配。包括 china.news 和 japan.news
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue1"),
            exchange = @Exchange(name = "itcast.topic"),
            key = {"china.#"}
    ))
    public void listenTopicQueue1(String msg){
        System.out.println("消费者接收到了topic.queue1的消息:"+msg);
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue2"),
            exchange = @Exchange(name = "itcast.topic"),
            key = {"#.news"}
    ))
    public void listenTopicQueue2(String msg){
        System.out.println("消费者接收到了topic.queue2的消息:"+msg);
    }
    /**
     * topic
     * 向交换机发送消息
     */
    @Test
    public void testSendTopicExchange(){
        // 交换机
        String exchange = "itcast.topic";
        String message = "hello china";
        rabbitTemplate.convertAndSend(exchange,"china.news",message);
    }

消息转换器

Spring 会把你发送的消息序列化为字节发送给 MQ,接收消息的时候,还会把字节反序列化为 Java 对象。

默认情况下 Spring 采用的序列化方式是 JDK 序列化。

我们可以去试一下效果

    @Bean
    public Queue objectQueue(){
        return new Queue("object.queue");
    }
    @RabbitListener(queues = "object.queue")
    public void listenObjectQueue(Map msg){
        System.out.println("接收到了object.queue的消息:"+msg);
    }
    @Test
    public void testSendObject(){
        Map msg = new HashMap<>();
        msg.put("name","lqh");
        msg.put("age",22);
        rabbitTemplate.convertAndSend("object.queue",msg);
    }

众所周知,JDK序列化存在下列问题:

  • 数据体积过大
  • 有安全漏洞
  • 可读性差

我们推荐可以使用 JSON 来序列化

在 publisher 和 consumer 两个服务中都引入依赖


    com.fasterxml.jackson.dataformat
    jackson-dataformat-xml
    2.9.10

配置消息转换器。

在各自的启动类中添加一个 Bean 即可

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

    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}
@SpringBootApplication
public class PublisherApplication {
    public static void main(String[] args) {
        SpringApplication.run(PublisherApplication.class);
    }

    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

转载自:微服务技术栈 - 乐心湖's Blog | 技术小白的技术博客 (xn2001.com)

你可能感兴趣的:(springcloud)