6-RabbitMQ实战

文章目录

1. 消息中间件概述

1.1 MQ简介

1.1.1 什么是消息中间件

MQ全称为Message Queue,消息队列是一种程序间的异步通信方式。是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信。

image-20210312171534774

1.1.2为什么使用MQ

在项目中,可将一些无需即时返回且耗时的操作提取出来,进行异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而提高系统吞吐量

1.1.3消息队列(MQ)应用场景

1、任务异步处理

将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间。

image-20210312172304257

同步方式的问题:当一个用户提交订单到成功需要300ms+300ms+300ms+20ms = 920ms,这是不能容忍的。也就是说库存、支付、物流、最后保存数据库全部成功,订单的提交才算完成

异步方式:

image-20210312172632817

用户点击完下单按钮后,只需等待25ms就能得到下单响应 (20 + 5 = 25ms)。也就是说,订单消息提交到MQ,MQ回馈一个消息成功,然后再把订单提交到数据库20ms,就完成了。至于MQ通知库存、支付、物流系统所花费的时间和订单系统成功没有关系了。后
提升用户体验和系统吞吐量(单位时间内处理请求的数目)。

2、应用程序解耦合

MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合。

image-20210312171738311

系统的耦合性越高,容错性就越低,可维护性就越低

image-20210312172014512

使用 MQ 使得应用间解耦,提升容错性和可维护性。库存和支付和物流直接去MQ取到订单的信息即可,即使库存系统报错,没关系,等到库存修复后再次从MQ中去取就可以了

3、削峰填谷

如订单系统,在下单的时候就会往数据库写数据。但是数据库只能支撑每秒1000左右的并发写入,并发量再高就容易宕机。低峰期的时候并发也就100多个,但是在高峰期时候,并发量会突然激增到5000以上,这个时候数据库肯定卡死了。但不一定宕机,只会很慢,一旦宕机就会有消息丢失

image-20210312173150805

image-20210312173339481

消息被MQ保存起来了,5000条数据对于MQ,简直是小意思,然后系统就可以按照自己的消费能力来消费,比如每秒1000个数据,这样慢慢写入数据库,这样就不会卡死数据库了。

但是使用了MQ之后,限制消费消息的速度为1000,但是这样一来,高峰期产生的数据势必会被积压在MQ中,高峰就被“削”掉了。但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000QPS,直到消费完积压的消息,这就叫做“填谷”

1.1.4 MQ的劣势

1、系统可用性降低
系统引入的外部依赖越多,系统稳定性越差。一旦 MQ 宕机,就会对业务造成影响。如何保证MQ的高可用?

2、系统复杂度提高
MQ 的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过 MQ 进行异步调用。如何保证消息没有被重复消费?怎么处理消息丢失情况?那么保证消息传递的顺序性?

3、一致性问题
A 系统处理完业务,通过 MQ 给B、C、D三个系统发消息数据,如果 B 系统、C 系统处理成功,D 系统处理失败。如何保证消息数据处理的一致性?

既然 MQ 有优势也有劣势,那么使用 MQ 需要满足什么条件呢?

生产者不需要从消费者处获得反馈。引入消息队列之前的直接调用,其接口的返回值应该为空,这才让明明下层的动作还没做,上层却当成动作做完了继续往后走,即所谓异步成为了可能。
容许短暂的不一致性。
确实是用了有效果。即解耦、提速、削峰这些方面的收益,超过加入MQ,管理MQ这些成本。

1.1.5常见的 MQ 产品

  • ActiveMQ:基于JMS
  • RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好
  • RocketMQ:基于JMS,阿里巴巴产品,目前交由Apache基金会
  • Kafka:分布式消息系统,高吞吐量

1.2 AMQP 和 JMS

MQ是消息通信的模型;实现MQ的大致有两种主流方式:AMQP、JMS。

1.2.1. AMQP

AMQP是一种协议,Advanced Message Queuing Protocol(高级消息队列协议),是一个网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。2006年,AMQP 规范发布。类比HTTP

image-20210315082555386

1.2.2. JMS

JMS即Java消息服务(JavaMessage Service)应用程序接口,好比java提供一套jdbc的接口API,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。

1.2.3. AMQP 与 JMS 区别

  • JMS是定义了统一的接口,来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式
  • JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的。
  • JMS规定了两种消息模式;而AMQP的消息模式更加丰富

1.3 RabbitMQ简介

RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message Queue 高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛。2007年,Rabbit 技术公司基于 AMQP 标准开发的 RabbitMQ 1.0 发布。 (Erlang 语言由 Ericson 设计,专门为开发高并发和分布式系统的一种语言,在电信领域使用广泛)

RabbitMQ官方地址:http://www.rabbitmq.com/

RabbitMQ 基础架构如下图:

image-20210315082502548

Broker:接收和分发消息的应用,就是一个中介,RabbitMQ Server就是 Message Broker
Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost 创建 exchange/queue 等
Connection:publisher/consumer 和 broker 之间的 TCP 连接
Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的 channel 进行通讯,AMQP method 包含了channel id 帮助客户端和message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销

Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)
Queue:消息最终被送到这里等待 consumer 取走
Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据

RabbitMQ提供了6种模式:简单模式,work模式,Publish/Subscribe发布与订阅模式,Routing路由模式,Topics主题模式,RPC远程调用模式(远程调用,不太算MQ;暂不作介绍);

官网对应模式介绍:https://www.rabbitmq.com/getstarted.html 点击手册按钮 RabbitMQ Tutorials

1555988678324

2.AMQP

2.1. 相关概念介绍

AMQP 一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。

AMQP是一个二进制协议,拥有一些现代化特点:多信道、协商式,异步,安全,扩平台,中立,高效。

RabbitMQ是AMQP协议的Erlang的实现。

概念说明
连接Connection一个网络连接,比如TCP/IP套接字连接。
会话Session端点之间的命名对话。在一个会话上下文中,保证“恰好传递一次”。
信道Channel多路复用连接中的一条独立的双向数据流通道。为会话提供物理传输介质。
客户端ClientAMQP连接或者会话的发起者。AMQP是非对称的,客户端生产和消费消息,服务器存储和路由这些消息。
服务节点Broker消息中间件的服务节点;一般情况下可以将一个RabbitMQ Broker看作一台RabbitMQ 服务器。
端点AMQP对话的任意一方。一个AMQP连接包括两个端点(一个是客户端,一个是服务器)。
消费者Consumer一个从消息队列里请求消息的客户端程序。
生产者Producer一个向交换机发布消息的客户端应用程序。

2.2. RabbitMQ运转流程

在入门案例中:

  • 生产者发送消息
    1. 生产者创建连接(Connection),开启一个信道(Channel),连接到RabbitMQ Broker;
    2. 声明队列并设置属性;如是否排它,是否持久化,是否自动删除;
    3. 将路由键(空字符串)与队列绑定起来;
    4. 发送消息至RabbitMQ Broker;
    5. 关闭信道;
    6. 关闭连接;
  • 消费者接收消息
    1. 消费者创建连接(Connection),开启一个信道(Channel),连接到RabbitMQ Broker
    2. 向Broker 请求消费相应队列中的消息,设置相应的回调函数;
    3. 等待Broker回应闭关投递响应队列中的消息,消费者接收消息;
    4. 确认(ack,自动确认)接收到的消息;
    5. RabbitMQ从队列中删除相应已经被确认的消息;
    6. 关闭信道;
    7. 关闭连接;

1565105223969

2.3. 生产者流转过程说明

  1. 客户端与代理服务器Broker建立连接。会调用newConnection() 方法,这个方法会进一步封装Protocol Header 0-9-1 的报文头发送给Broker ,以此通知Broker 本次交互采用的是AMQPO-9-1 协议,紧接着Broker 返回Connection.Start 来建立连接,在连接的过程中涉及Connection.Start/.Start-OK 、Connection.Tune/.Tune-Ok ,Connection.Open/ .Open-Ok 这6 个命令的交互。
  2. 客户端调用connection.createChannel方法。此方法开启信道,其包装的channel.open命令发送给Broker,等待channel.basicPublish方法,对应的AMQP命令为Basic.Publish,这个命令包含了content Header 和content Body()。content Header 包含了消息体的属性,例如:投递模式,优先级等,content Body 包含了消息体本身。
  3. 客户端发送完消息需要关闭资源时,涉及到Channel.Close和Channel.Close-Ok 与Connetion.Close和Connection.Close-Ok的命令交互。

生产者流转过程图

3.4. 消费者流转过程说明

  1. 消费者客户端与代理服务器Broker建立连接。会调用newConnection() 方法,这个方法会进一步封装Protocol Header 0-9-1 的报文头发送给Broker ,以此通知Broker 本次交互采用的是AMQPO-9-1 协议,紧接着Broker 返回Connection.Start 来建立连接,在连接的过程中涉及Connection.Start/.Start-OK 、Connection.Tune/.Tune-Ok ,Connection.Open/ .Open-Ok 这6 个命令的交互。
  2. 消费者客户端调用connection.createChannel方法。和生产者客户端一样,协议涉及Channel . Open/Open-Ok命令。
  3. 在真正消费之前,消费者客户端需要向Broker 发送Basic.Consume 命令(即调用channel.basicConsume 方法〉将Channel 置为接收模式,之后Broker 回执Basic . Consume - Ok 以告诉消费者客户端准备好消费消息。
  4. Broker 向消费者客户端推送(Push) 消息,即Basic.Deliver 命令,这个命令和Basic.Publish 命令一样会携带Content Header 和Content Body。
  5. 消费者接收到消息并正确消费之后,向Broker 发送确认,即Basic.Ack 命令。
  6. 客户端发送完消息需要关闭资源时,涉及到Channel.Close和Channl.Close-Ok 与Connetion.Close和Connection.Close-Ok的命令交互。

image-20220111113635533

2. 安装及配置RabbitMQ

RabbitMQ 官方地址:http://www.rabbitmq.com/

2.1CentOS7 安装RabbitMQ

第一、上传erlang-19.0.4-1.el7.centos.x86_64.rpm和rabbitmq-server-3.6.6-1.el7.noarch.rpm到linux

第二、安装erlang:

rpm -ivh erlang-19.0.4-1.el7.centos.x86_64.rpm

测试是否安装成功:

image-20210902185452858

第三、安装socat:

yum install socat

第四、安装rabbitmq:

rpm -ivh rabbitmq-server-3.6.6-1.el7.noarch.rpm

如果有进程占用yum,直接强制关闭 rm -f /var/run/yum.pid

启动命令

service rabbitmq-server start
service rabbitmq-server stop
service rabbitmq-server restart

开启web管理界面

rabbitmq-plugins enable rabbitmq_management

然后重启RabbitMQ:

然后关闭RabbitMQ

修改配置文件

将配置文件模板复制到etc目录:

cp /usr/share/doc/rabbitmq-server-3.6.6/rabbitmq.config.example /etc/rabbitmq/rabbitmq.config

通过vim命令编辑:

vim /etc/rabbitmq/rabbitmq.config

修改下面内容:

image-20210902185835432

注意要去掉后面的逗号

启动

service rabbitmq-server start

然后在主机中通过地址:http://192.168.88.137:15672/即可访问到管理界面

启动报错解决办法:

image-20210902185955387

vi /etc/rabbitmq/rabbitmq-env.conf 

在文件里面添加这一行:NODENAME=rabbit@localhost,保存

(注意:rabbitmq-env.conf这个文件没有,打开之后自动创建)

进入/var/lib/rabbitmq/mnesia

删除mnesia文件夹下的所以文件

然后启动

service rabbitmq-server start

image-20210902190055824

2.2Docker安装RabbitMQ

一、获取镜像
docker pull rabbitmq
二、运行镜像
#方式一:默认guest 用户,密码也是 guest
docker run -d --hostname my-rabbit --name rabbit -p 15672:15672 -p 5672:5672 rabbitmq:management

#方式二:设置用户名和密码
docker run -d --hostname my-rabbit --name rabbit -e RABBITMQ_DEFAULT_USER=user -e RABBITMQ_DEFAULT_PASS=password -p 15672:15672 -p 5672:5672 rabbitmq:management

进入容器中

docker exec -it 容器id bash

进入后执行如下命令,开启web管理界面

rabbitmq-plugins enable rabbitmq_management
三、访问ui页面
http://39.105.140.192:15672/

2.3docker-compose安装RabbitMQ

一.创建RabbitMQ的docker-compose.yml文件

version: '3'
services:
  rabbitmq:
    image: rabbitmq
    container_name: rabbitmq
    restart: always
    hostname: myRabbitmq
    ports:
      - 15672:15672
      - 5672:5672
    volumes:
      - /rabbitmq/data:/var/lib/rabbitmq
    environment:
      - RABBITMQ_DEFAULT_USER=root
      - RABBITMQ_DEFAULT_PASS=root

二.进入容器中

docker exec -it 容器id bash

三.进入后执行如下命令,开启web管理界面

rabbitmq-plugins enable rabbitmq_management

四.Docker部署rabbitmq遇到的两个问题

1.背景

Docker部署rabbitmq遇到的如下两个问题

问题一:访问交换机时报错

Management API returned status code 500

问题二:访问channel时报错

Stats in management UI are disabled on this node

image-20210906173700760

[root@iZ2ze11jjfjafkhgptqnmvZ ~]# docker exec -it 7f79909827a0  bash
root@myRabbitmq:/# cd /etc/rabbitmq/conf.d/
root@myRabbitmq:/etc/rabbitmq/conf.d# echo management_agent.disable_metrics_collector = false > management_agent.disable_metrics_collector.conf
root@myRabbitmq:/etc/rabbitmq/conf.d# exit
exit
[root@iZ2ze11jjfjafkhgptqnmvZ ~]# docker ps
CONTAINER ID   IMAGE                 COMMAND                  CREATED      STATUS      PORTS                                                                                                                                      NAMES
7f79909827a0   rabbitmq              "docker-entrypoint.s…"   4 days ago   Up 4 days   4369/tcp, 0.0.0.0:5672->5672/tcp, :::5672->5672/tcp, 5671/tcp, 15691-15692/tcp, 25672/tcp, 0.0.0.0:15672->15672/tcp, :::15672->15672/tcp   rabbitmq
67fc99752767   portainer/portainer   "/portainer"             4 days ago   Up 4 days   0.0.0.0:9000->9000/tcp, :::9000->9000/tcp                                                                                                  portainer
[root@iZ2ze11jjfjafkhgptqnmvZ ~]# docker restart 7f79909827a0
7f79909827a0
[root@iZ2ze11jjfjafkhgptqnmvZ ~]# 

3 管理界面介绍

第一次访问需要登录,默认的账号密码为:guest/guest

主页

image-20210902203246077

添加用户

image-20210902203356842

上面的Tags选项,其实是指定用户的角色,可选的有以下几个:

超级管理员(administrator)

可登陆管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作。

监控者(monitoring)

可登陆管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)

策略制定者(policymaker)

可登陆管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息(上图红框标识的部分)。

普通管理者(management)

仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理。

其他

无法登陆管理控制台,通常就是普通的生产者和消费者。

添加虚拟主机(Virtual Hosts)

为了让各个用户可以互不干扰的工作,RabbitMQ添加了虚拟主机(Virtual Hosts)的概念。其实就是一个独立的访问路径,不同用户使用不同路径,各自有自己的队列、交换机,互相不会影响。

image-20210902203508810

image-20210902203519342

若启动报错

则进入/var/lib/rabbitmq/mnesia

删除mnesia文件夹下的所以文件

4 SpringBoot整合Rabbitmq

提示
我们分发送和接收 2 部分来学习 Spring Boot 和 RabbitMQ 的整合

1在 pom.xml 中引入 spring-boot-starter-amqp

image-20210906155646525

注意
虽然你在界面上选择的是 RabbitMQ ,但是本质上引入的是 AMQP ,因为 RabbitMQ 是 AMQP 的一种实现,也是默认实现。

2.启用自动配置

老规矩,使用 @EnableRabbit 注解标注于配置类上,以表示使用 RabbitMQ 的注解功能。

3.配置文件

配置 RabbitMQ 的连接地址、端口以及账户信息:

spring:
  rabbitmq:
    host: 39.105.140.192
    username: root
    password: root
    virtual-host: /

4.配置类

@Configuration
public class RabbitMQConfig {

    @Bean
    public Queue queue(){
        return new  Queue("queue1");
    }
}

参数说明:

参数说明
name字符串值,exchange 的名称。
durable布尔值,表示该 queue 是否持久化。 持久化意味着当 RabbitMQ 重启后,该 queue 是否会恢复/仍存在。 另外,需要注意的是,queue 的持久化不等于其中的消息也会被持久化。
exclusive布尔值,表示该 queue 是否排它式使用。排它式使用意味着仅声明他的连接可见/可用,其它连接不可见/不可用。
autoDelete布尔值,表示当该 queue 没“人”(connection)用时,是否会被自动删除。

不指定 durable、exclusive 和 autoDelete 时,默认为 truefalsefalse 。表示持久化、非排它、不用自动删除。

5.消息生产者

@RestController
public class MqController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @RequestMapping("sendOne")
    public void sendOne(){
        for (int i=0;i<1000;i++){
            //第一个参数 队列的名称;  第二个参数 要发送的消息
            amqpTemplate.convertAndSend("queue1","hello rabbitmq----");
        }
    }
}

6.消息消费者

@Component
public class MqRecive {

    @RabbitListener(queues = "queue1")
    public void listen(String msg) throws InterruptedException {
        Thread.sleep(100);
        System.out.println(msg);
    }
}

5 交换机类型

Direct exchange(直连交换机)
Fanout exchange(扇型交换机)
Topic exchange(主题交换机)
Headers exchange(头交换机)(了解)

交换机核心作用:分发路由消息、中转
队列:容器 存放多个不同消息 遵循先进先出的原则

1.扇型交换机 Fanout exchange

只要队列绑定同一个交换机,生产者将消息投递到交换机中,交换机会将消息发送给所有绑定的队列.跟路由键无关.

扇形交换机应用于广播消息模式

2 直连交换机 Direct exchange

根据生产者投递不同的路由键,交换机匹配路由键发送到具体的队列中存放消息。

直连交换机应用于Routing路由消息模式

3 主题交换机 Topic Exchange

Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符

主题交换机应用于Topics通配符模式

6 五种消息模型(了解)

RabbitMQ提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此不掌握。那么也就剩下5种。

1.基本消息模型

RabbitMQ是一个消息代理:它接受和转发消息。 你可以把它想象成一个邮局:当你把邮件放在邮箱里时,你可以确定邮差先生最终会把邮件发送给你的收件人。 在这个比喻中,RabbitMQ是邮政信箱,邮局和邮递员。

image-20210903163345608

P(producer/ publisher):生产者,一个发送消息的用户应用程序。

C(consumer):消费者,消费和接收有类似的意思,消费者是一个主要用来等待接收消息的用户应用程序

队列(红色区域):rabbitmq内部类似于邮箱的一个概念。虽然消息流经rabbitmq和你的应用程序,但是它们只能存储在队列中。队列只受主机的内存和磁盘限制,实质上是一个大的消息缓冲区。许多生产者可以发送消息到一个队列,许多消费者可以尝试从一个队列接收数据。

总之:

生产者将消息发送到队列,消费者从队列中获取消息,队列是存储消息的缓冲区。

@Configuration
public class RabbitMQConfig {

    @Bean
    public Queue queue1(){
        return new  Queue("queue1");
    }
}
	//生产者
@RestController
public class MqController {

    @Autowired
    private AmqpTemplate amqpTemplate;

    @RequestMapping("sendOne")
    public void sendOne(){
        for (int i=0;i<1000;i++){
            //第一个参数 队列的名称;  第二个参数 要发送的消息
            amqpTemplate.convertAndSend("queue1","hello rabbitmq----");
        }
    }
}
	//消费者
@Component
public class MqRecive {

    @RabbitListener(queues = "queue1")
    public void listen(String msg) throws InterruptedException {
        Thread.sleep(100);
        System.out.println(msg);
    }
}

2.work消息模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kjYNIQgV-1644564430537)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220113155601129.png)]

Work Queues与入门程序的简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。它们处于竞争者的关系,一条消息只会被一个消费者接收,rabbit采用轮询的方式将消息是平均发送给消费者的;消费者在处理完某条消息后,才会收到下一条消息。

应用场景:对于 任务过重或任务较多情况使用工作队列可以提高任务处理的速度。如生产者生产一千条消息,那么c1和c2各消费500条,队列消费消息是均衡分配

面试题:避免消息堆积?

1) 采用workqueue,多个消费者监听同一队列。

2)接收到消息以后,而是通过线程池,异步消费。

@Configuration
public class RabbitMQConfig {

    @Bean
    public Queue queue1(){
        return new  Queue("queue1");
    }
}
    //消息生产者
    @RequestMapping("sendOne")
    public void sendOne(){
        for (int i=0;i<1000;i++){
            //第一个参数 队列的名称;  第二个参数 要发送的消息
            amqpTemplate.convertAndSend("queue1","hello rabbitmq----");
        }
    }
@Component
public class MqRecive {
	//消费者1
    @RabbitListener(queues = "queue1")
    public void listen1(String msg) throws InterruptedException {
        Thread.sleep(100);
        System.out.println("消费者1接收消息---------"+msg);
    }
	//消费者2
    @RabbitListener(queues = "queue1")
    public void listen2(String msg) throws InterruptedException {
        Thread.sleep(100);
        System.out.println("消费者2接收消息*********"+msg);
    }
}

3.广播模式-fanout

广播模式–也称发布与订阅模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MaRmLFxi-1644564430538)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220113121642547.png)]

生产者将消息投递到交换机中,交换机会将消息发送给所有绑定到该交换机的队列。

配置类

package com.example.config;


import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {
    /**
     *声明sms队列
     */
    @Bean
    public Queue smsQueue(){
        return new Queue("sms_queue");
    }
    /**
     * 声明email队列
     */
    @Bean
    public Queue emailQueue(){
        return new Queue("email_queue");
    }

    /**
     *声明扇形交换机fanout-exchange-1
     */
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("fanout-exchange-1");
    }

    /**
     * 将smsQueue和fanout-exchange-1建立绑定关系
     * @param smsQueue 短信队列
     * @param fanoutExchange 交换机对象
     * @return
     */
    @Bean
    public Binding binding1(Queue smsQueue,FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(smsQueue).to(fanoutExchange);
    }

    /**
     * 将emailQueue和fanout-exchange-1建立绑定关系
     * @param emailQueue 邮件队列
     * @param fanoutExchange 交换机对象
     * @return
     */
    @Bean
    public Binding binding2(Queue emailQueue,FanoutExchange fanoutExchange){
        return BindingBuilder.bind(emailQueue).to(fanoutExchange);
    }
}

生产者

package com.example;

import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ProducerApplicationTests {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void sendMsg(){
        //第一个参数交换机名称 第二个参数路由key 第三个参数 消息
        rabbitTemplate.convertAndSend("fanout-exchange-1","","hello 我是消息***");

    } 
}

消费者

@Component
public class MqRecive {

    @RabbitListener(queues = "sms_queue")
    public void listen1(String msg) throws InterruptedException {
        System.out.println("消费者1接收消息-------------"+msg);
    }

    @RabbitListener(queues = "email_queue")
    public void listen2(String msg) throws InterruptedException {
        System.out.println("消费者2接收消息-------------"+msg);
    }
}

4.Routing路由模式

路由模式特点:

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

image-20220113172036391

图解:

  • P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
  • X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列
  • C1:消费者,其所在队列指定了需要routing key 为 error 的消息
  • C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息

配置类

package com.example.config;


import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {
    /**
     *声明sms队列
     */
    @Bean
    public Queue smsQueue(){
        return new Queue("sms_queue");
    }
    /**
     * 声明email队列
     */
    @Bean
    public Queue emailQueue(){
        return new Queue("email_queue");
    }

    /**
     *声明直连交换机direct-exchange-1
     */
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange("direct-exchange-1");
    }

    /**
     * 将smsQueue和direct-exchange-1建立绑定关系
     * @param smsQueue 短信队列
     * @param directExchange 交换机对象
     * @return
     */
    @Bean
    public Binding binding1(Queue smsQueue,DirectExchange directExchange) {
        return BindingBuilder.bind(smsQueue).to(directExchange).with("sms");
    }

    /**
     * 将emailQueue和direct-exchange-1建立绑定关系
     * @param emailQueue 邮件对象
     * @param directExchange 交换机对象
     * @return
     */
    @Bean
    public Binding binding2(Queue emailQueue,DirectExchange directExchange){
        return BindingBuilder.bind(emailQueue).to(directExchange).with("email");
    }
}

生产者

package com.example;

import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ProducerApplicationTests {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void sendMsg(){
        //第一个参数交换机名称 第二个参数路由key 第三个参数 消息
        rabbitTemplate.convertAndSend("direct-exchange-1","sms","hello 我是短信消息***");
        rabbitTemplate.convertAndSend("direct-exchange-1","email","hello 我是邮件消息***");
    }

}

消费者

package com.example.consumer;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class MqRecive {

    @RabbitListener(queues = "sms_queue")
    public void listen1(String msg) throws InterruptedException {
        System.out.println("短信队列消费者接收消息-------------"+msg);
    }

    @RabbitListener(queues = "email_queue")
    public void listen2(String msg) throws InterruptedException {
        System.out.println("邮件队列消费者接收消息-------------"+msg);
    }
}

5.Topics通配符模式

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

Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert

通配符规则:

#号表示支持匹配多个词
*号表示只能匹配一个词

举例:

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

item.*:只能匹配item.insert

image-20220113180923820

配置类

package com.example.config;


import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {
    /**
     *声明sms队列
     */
    @Bean
    public Queue smsQueue(){
        return new Queue("sms_queue");
    }
    /**
     * 声明email队列
     */
    @Bean
    public Queue emailQueue(){
        return new Queue("email_queue");
    }

    /**
     * 声明主题交换机topic-exchange-1
     */
    @Bean
    public TopicExchange topicExchange(){
        return new TopicExchange("topic-exchange-1");
    }

    /**
     * 将smsQueue和topic-exchange-1建立绑定关系
     * @param smsQueue 短信队列
     * @param topicExchange 交换机对象
     * @return
     */
    @Bean
    public Binding binding1(Queue smsQueue,TopicExchange topicExchange) {
        return BindingBuilder.bind(smsQueue).to(topicExchange).with("*.sms.*");
    }

    /**
     * 将emailQueue和topic-exchange-1建立绑定关系
     * @param emailQueue 邮件对象
     * @param topicExchange 交换机对象
     * @return
     */
    @Bean
    public Binding binding2(Queue emailQueue,TopicExchange topicExchange){
        return BindingBuilder.bind(emailQueue).to(topicExchange).with("email.#");
    }

}

生产者

package com.example;

import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ProducerApplicationTests {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void sendMsg(){
        //第一个参数交换机名称 第二个参数路由key 第三个参数 消息
        rabbitTemplate.convertAndSend("topic-exchange-1","chen.sms.yun","hello 我是短信消息***");
        rabbitTemplate.convertAndSend("topic-exchange-1","email.chen.yun","hello 我是邮件消息***");
    }

}

消费者

package com.example.consumer;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class MqRecive {

    @RabbitListener(queues = "sms_queue")
    public void listen1(String msg) throws InterruptedException {
        System.out.println("短信队列消费者接收消息-------------"+msg);
    }

    @RabbitListener(queues = "email_queue")
    public void listen2(String msg) throws InterruptedException {
        System.out.println("邮件队列消费者接收消息-------------"+msg);
    }
}

6. 模式总结

RabbitMQ工作模式:
1、简单模式 HelloWorld
一个生产者、一个消费者,不需要设置交换机(使用默认的交换机)

2、工作队列模式 Work Queue
一个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机)

3、发布订阅模式 Publish/subscribe
需要设置类型为fanout的交换机,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到所有绑定的队列

4、路由模式 Routing
需要设置类型为direct的交换机,交换机和队列进行绑定,并且指定routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列

5、通配符模式 Topic
需要设置类型为topic的交换机,交换机和队列进行绑定,并且指定通配符方式的routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列

7.PULL型消费者

PULL 型消费『不依靠』@RabbitListener 和 @RabbitHandler 注解。而是需要在代码中手动调用 .receiveAndConvert 方法,主动从 RabbitMQ上取消息。

package com.example;

import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ConsumerApplicationTests {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void contextLoads() {
        Object sms_queue = rabbitTemplate.receiveAndConvert("email_queue");
        System.out.println(sms_queue);
    }
}

8. 发送者确认

思考
发送者如何知道自己所发送的消费成功抵达了 RabbitMQ Broker 中的 Exchange 中,乃至成功抵达了 RabbitMQ Broker 中的 Queue 中?

确认消息已到 Exchange

RabbitMQ 有一个配置属性 spring.rabbitmq.publisher-confirm-type 控制是否开启确认功能,有三种确认类型,

  • 该属性默认值是 NONE ,表示不开启消息确认。
  • 当改属性的值为 CORRELATED 时,表示支持以异步回调方式获得确认与否的信息。
  • SIMPLE值经测试有两种效果,其一效果和CORRELATED值一样会触发回调方法

核心代码

spring:
  rabbitmq:
    host: 39.105.140.192
    username: root
    password: root
    virtual-host: /
    ###开启消息确认机制
    publisher-confirm-type: CORRELATED
    publisher-returns: true

发送消息

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendMsg(String msgJson) {
        //携带消息
        CorrelationData correlationData = new CorrelationData();
        correlationData.setId(msgJson);
        String orderExchange = "order_exchange_name";
        String orderRoutingKey = "orderRoutingKey";
        rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, msgJson, correlationData);
    }

发送者确认

package com.example.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@Slf4j
public class RabbitMqCallBackConfig {

    @Bean
    public RabbitTemplate getRabbitTemplate(ConnectionFactory connectionFactory){
        RabbitTemplate rabbitTemplate = new RabbitTemplate();
        rabbitTemplate.setConnectionFactory(connectionFactory);
        // 设置开启 Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
        rabbitTemplate.setMandatory(true);

        // 确认消息已发送到交换机(Exchange)
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                String data = correlationData.getId();
                if(ack){
                    log.debug("消息[{}]成功发送至 Exchange,将改变其状态至【发送成功】", data);
                }else{
                    log.info("发送失败");
                }
            }
        });
        return rabbitTemplate;
    }
}

9 开启手动ack

1 简介

当RabbitMQ知道消息被消费者接收,队列中的消息就会被删除。RabbitMQ怎么知道消息被接收了呢?

这就要通过消息确认机制ack(Acknowlege)来实现了。当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接收。不过这种回执ACK分两种情况:

  • 自动ACK:消息一旦被接收,消费者自动发送ACK
  • 手动ACK:消息接收后,不会发送ACK,需要手动调用

这两ACK要怎么选择呢?这需要看消息的重要性:

  • 如果消息不太重要,丢失也没有影响,那么自动ACK会比较方便
  • 如果消息非常重要,那么最好在消费完成后手动ACK

2 SpringBoot开启手动ACK

配置文件中要开启手动ack

spring:
  rabbitmq:
    host: 39.105.140.192
    username: root
    password: root
    virtual-host: /
    listener:
      simple:
        acknowledge-mode: manual
package com.example.subscribe;

import com.example.dao.ReliableMapper;
import com.example.entity.Reliable;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@Slf4j
public class SubscribeMessage {

    @RabbitListener(queues = "message_queue")
    public void receiverMessage(Message message, Channel channel) throws IOException {
        Integer msg = Integer.parseInt(new String(message.getBody()));
        log.info(msg.toString());

        try{
            //把收到的消息插入数据库
        }catch (Exception e){
            //如果插入失败,证明队列已经有该条消息了.拒签消息,从队列中删除消息。注意参数为false
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
            return;
        }

        try {
            //处理本地业务
            // 确认接收,并处理该消息:
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        }catch (Exception e){
            //本地业务处理失败,告诉队列重发消息
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}

10 消费者重试机制

1 RabbitMQ重试机制触发:

1.当消费者在处理消息的时候,程序抛出异常情况下,就不断重试;

2.应该对消息重试设置间隔重试时间,比如消费失败最多只能重试5次,间隔3s;
重试多次还是失败的情况下,存放到死信队列或者存放到数据库表中记录后期人工补偿。
间隔是为了防止重复消费的问题(为了解决幂等问题)

原理:

在重试的过程中使用aop拦截消费者监听方法,在重试过程中不会打印错误日志,如果重试多次失败直到达到最大次数才会控制台打印日志。

SpringBoot开启重试策略

spring:
  rabbitmq:
    host: 39.105.140.192
    username: root
    password: root
    virtual-host: /
    listener:
      simple:
        retry:
          ####开启消费者(程序出现异常的情况下会)进行重试
          enabled: true
          ####最大重试次数
          max-attempts: 5
          ####重试间隔次数
          initial-interval: 3000

消费者代码

    @RabbitListener(queues = "email_queue")
    public void listen2(String msg) throws InterruptedException {
        System.out.println("邮件队列消费者接收消息-------------"+msg);
        int i=1/0;
        System.out.println("失败");
    }

此时启动程序,发送消息后可以看到控制台输出内容如下:

image-20220120115705989

2 消费者被重试多次还是失败如何处理

如果重试5次,还是失败的情况下应该如何处理呢?
默认情况下,重试多次还是失败会自动删除该消息(消息可能会丢失)
解决思路:

  1. 如果重试多次还是失败的情况下,最终存放到死信队列;
  2. 采用表日志记录,消费失败的错误日志记录,后期人工对该消息实现补偿。

11 死信队列

1 消息中间件产生了消息堆积如何解决

产生的背景:如果没有及时的消费者消费消息,生产者一直不断往队列服务器存放消息,最终会导致消息堆积。

两种场景:

  1. 没有消费者消费的情况下:死信队列、设置消息有效期
    相当于对消息设置有效期,在规定的时间内如果没有消费的话,自动过期,过期的时候会执行客户端回调监听的方法将消息存放到数据库记录,后期实现补偿。
  2. 有一个消费者消费的情况:应该提高消费能力,消费者实现集群

2 RabbitMQ如何保证消息不丢失

1.MQ服务器端应该消息持久化到硬盘
2.生产者使用消息确认机制百分百能够将消息投递到MQ成功
3.消费者使用手动ack机制确认消息百分百消费成功

如果队列容量满了,再继续投递可能会丢失。解决:死信队列
死信队列:备胎队列,消息中间件队列因为某种原因拒绝存放该消息,可以转移到死信队列中存放。
死信队列产生的背景:
1.生产者投递消息到MQ中,消息过期了;
2.队列已经达到最大长度(队列存放消息满了)MQ拒绝接受存放该消息;
3.消费者多次消费该消息失败的情况,也会存放到死信队列;

应用

1.例如,订单 30 分钟内未支付则需要取消订单。

2.在某种程度上,它可以替代定时任务

3.简介

死信处理过程

​ 死信队列,英文缩写:DLX 。Dead Letter Exchange(死信交换机),当消息在队列成为Dead message后,通过该队列把这条死信消息发给另一个交换机,这个交换机就是DLX。

image-20220209141212263

消息成为死信的三种情况(面试常问):
  • 消息被拒绝(basic.reject / basic.nack),并且requeue = false
  • 消息TTL过期
  • 队列达到最大长度
队列绑定死信交换机:

给队列设置参数: x-dead-letter-exchange 和 x-dead-letter-routing-key

image-20220209141516805

5.RabbitMQ整合死信队列

配置类

 DLX 死信队列 步骤
    1、声明正常的队列(order_queue) 和正常的交换机(order_exchange)
    2、声明死信队列(order_dlx_queue)和死信交换机(order_dlx_exchange)
    3、正常队列绑定死信交换机
        设置两个参数
            x-dead-letter-exchange:死信交换机的名称
            x-dead-letter-routing-key:发送给死信交换机的routingkey
package com.chenyun.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class DeadLetterMQConfig {
    /**
     * 订单交换机
     */
    private String orderExchange = "order_exchange";

    /**
     * 订单队列
     */
    private String orderQueue = "order_queue";

    /**
     * 订单路由key
     */
    private String orderRoutingKey = "order_routingKey";

    /**
     * 死信交换机
     */
    private String dlxExchange = "order_dlx_exchange";

    /**
     * 死信队列
     */
    private String dlxQueue = "order_dlx_queue";
    /**
     * 死信路由
     */
    private String dlxRoutingKey = "order_dlx_routingKey";

    /**
     * 声明死信交换机
     *
     * @return DirectExchange
     */
    @Bean
    public DirectExchange dlxExchange() {
        return new DirectExchange(dlxExchange);
    }

    /**
     * 声明死信队列
     *
     * @return Queue
     */
    @Bean
    public Queue dlxQueue() {
        return new Queue(dlxQueue);
    }

    /**
     * 声明订单业务交换机
     *
     * @return DirectExchange
     */
    @Bean
    public DirectExchange orderExchange() {
        return new DirectExchange(orderExchange);
    }

    /**
     * 声明订单队列 TTL(延迟,并绑定死信交换机)
     *
     * @return Queue
     */
    @Bean
    public Queue orderQueue() {
        Map<String, Object> arguments = new HashMap<>(2);
        // 绑定死信交换机
        arguments.put("x-dead-letter-exchange", dlxExchange);// 指定时期后消息投递给哪个交换器。
        // 绑定死信路由key
        arguments.put("x-dead-letter-routing-key", dlxRoutingKey);// 指定到期后投递消息时以哪个路由键进行投递
        arguments.put("x-message-ttl", 5000);                         // 指定到期时间。5 秒
        return new Queue(orderQueue, true, false, false, arguments);
    }

    /**
     * 绑定订单队列到订单交换机
     *
     * @return Binding
     */
    @Bean
    public Binding orderBinding() {
        return BindingBuilder.bind(orderQueue())
                .to(orderExchange())
                .with(orderRoutingKey);
    }
    /**
     * 绑定死信队列到死信交换机
     *
     * @return Binding
     */
    @Bean
    public Binding binding() {
        return BindingBuilder.bind(dlxQueue())
                .to(dlxExchange())
                .with(dlxRoutingKey);
    }
}

消息生产者

package com.chenyun.web;

import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DeadLetterController {

    @Autowired
    private RabbitTemplate rabbitTemplate;
    /**
     * 订单交换机
     */
    private String orderExchange = "order_exchange";
    /**
     * 订单路由key
     */
    private String orderRoutingKey = "order_routingKey";

    @RequestMapping("/sendOrderMsg")
    public String sendOrderMsg(){
        rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, "hello-chen");
        return "success";
    }
}

监听死信队列

package com.chenyun.customer;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class OrderDlxConsumer {

    /**
     * 监听死信队列
     *
     * @return
     */
    @RabbitListener(queues = "order_dlx_queue")
    public void orderConsumer(String msg) {
        System.out.println("死信队列获取消息:" + msg);
    }
}

监听订单队列

package com.chenyun.customer;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class OrderConsumer{
    /**
     * 监听订单队列
     *
     * @return
     */
    @RabbitListener(queues = "order_queue")
    public void orderConsumer(String msg) {
        System.out.println("订单队列获取消息:" + msg);
    }
}

运行结果:

image-20211116205608021


image-20211116205859022

总结:

  1. 死信交换机和死信队列和普通的没有区别
  2. 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列
  3. 消息成为死信的三种情况:
    • 队列消息长度到达限制;
    • 消费者拒绝消费消息,并且不重回队列;
    • 原队列存在消息过期设置,消息到达超时时间未被消费

12 RabbitMQ如何获取消费者结果(了解)

RabbitMQ异步如何获取消费结果?
多线程+主动查询。
消费的结果根据业务来定,假设业务场景是订单消费者消费成功将订单数据存入数据库,数据库能查到结果说明消费成功。

数据库

create table order_info(
	id int primary key auto_increment,
	order_name varchar(20),
	order_status int default 0, -- 0待支付 -- 1已支付
	order_id varchar(50)
);

application.yml

spring:
  rabbitmq:
    host: 39.105.140.192
    username: root
    password: root
    virtual-host: /
    publisher-confirm-type: CORRELATED
    publisher-returns: true

  datasource:
    url: jdbc:mysql://localhost:3306/rabbitmq?useUnicode=true&characterEncoding=UTF-8&&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    #开启下划线转驼峰
    mapUnderscoreToCamelCase: true


server:
  port: 8080

核心代码

package com.chenyun.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitmqConfig {
    /**
     * 订单交换机
     */
    private String orderExchange = "order_exchange";

    /**
     * 订单队列
     */
    private String orderQueue = "order_queue";

    /**
     * 订单路由key
     */
    private String orderRoutingKey = "order_routingKey";

    /**
     * 声明订单业务交换机
     *
     * @return DirectExchange
     */
    @Bean
    public DirectExchange orderExchange() {
        return new DirectExchange(orderExchange);
    }

    /**
     * 声明订单队列 TTL(延迟,并绑定死信交换机)
     *
     * @return Queue
     */
    @Bean
    public Queue orderQueue() {
        return new Queue(orderQueue);
    }

    /**
     * 绑定订单队列到订单交换机
     *
     * @return Binding
     */
    @Bean
    public Binding orderBinding() {
        return BindingBuilder.bind(orderQueue())
                .to(orderExchange())
                .with(orderRoutingKey);
    }
}
package com.chenyun.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
    private int id;
    private String orderName;
    private int orderStatus;
    private String orderId;

    public Order(String orderName, String orderId) {
        this.orderName = orderName;
        this.orderId = orderId;
    }
}
package com.chenyun.mapper;

import com.chenyun.pojo.Order;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

@Repository
public interface OrderMapper {
    @Insert("insert into order_info(order_name,order_id) values(#{orderName},#{orderId})")
    int addOrder(Order order);

    @Select("SELECT * from order_info where order_id=#{orderId} ")
    Order getOrder(String orderId);
}
package com.chenyun.web;

import com.chenyun.mapper.OrderMapper;
import com.chenyun.pojo.Order;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DeadLetterController {

    @Autowired
    private RabbitTemplate rabbitTemplate;
    /**
     * 订单交换机
     */
    private String orderExchange = "order_exchange";
    /**
     * 订单路由key
     */
    private String orderRoutingKey = "order_routingKey";

    @Autowired
    private OrderMapper orderMapper;

    @RequestMapping("/sendOrderMsg")
    public String sendOrderMsg() throws JsonProcessingException {
        String orderId = System.currentTimeMillis() + "";
        String orderName = "rabbitmq确认消息被消费";
        Order order = new Order(orderName, orderId);
        ObjectMapper mapper= new ObjectMapper();
        String jsonStr = mapper.writeValueAsString(order);
        sendMsg(jsonStr);
        return orderId;
    }

    // 如果用同步可以使用消息确认机制,必须投递成功再返回orderId,效率低
    @Async
    public void sendMsg(String msg) {
        rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, msg);
        // 消息投递失败
    }

    /**
     * 主动查询接口
     * 先查询该订单的消息是否投递失败
     * 再查询数据库
     */
    @RequestMapping("/getOrder")
    public Object getOrder(String orderId) {
        Order orderEntity = orderMapper.getOrder(orderId);
        if (orderEntity == null) {
            return "消息正在异步的处理中";
        }
        return orderEntity;
    }
}
package com.chenyun.customer;

import com.chenyun.mapper.OrderMapper;
import com.chenyun.pojo.Order;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderConsumer{
    @Autowired
    private OrderMapper orderMapper;

    /**
     * 监听订单队列
     *
     * @return
     */
    @RabbitListener(queues = "order_queue")
    public void orderConsumer(String msg) throws JsonProcessingException {
        System.out.println("订单队列获取消息:" + msg);
        ObjectMapper mapper= new ObjectMapper();
        Order order = mapper.readValue(msg, Order.class);
        if(order == null){
            return;
        }
        orderMapper.addOrder(order);
    }
}
package com.chenyun;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.chenyun.mapper")
public class Rabbitmq2Application {

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

13 RabbitMQ解决订单自动超时

1 订单30分钟自动超时,有哪些设计方案

订单30分钟未支付,系统自动超时关闭有哪些实现方案?
1.基于任务调度实现,效率非常低
2.基于Redis过期key实现
用户下单的时候,生成一个令牌(有效期30分钟),存放到redis中,token失效的时候(redis key过期)执行redis监听方法查询数据库表订单是否支付,如果没有支付关闭订单库存+1。
缺点:表中存放一个冗余字段记录token
3.基于MQ的延迟队列实现(最佳)

2 基于MQ延迟队列实现订单30分钟自动超时

实现原理: 死信队列

下单的时候往mq投递一个消息,设置有效期为30分钟,当该消息失效的时候(没有被消费的情况下),执行客户端的一个方法告诉该消息已经失效,这时候查询这笔订单是否有支付。

image-20211117195000242

死信队列应用场景:
1.投递消息存放到MQ中过期了;
2.投递消息到MQ中,队列容器满了;
3.消费者多次消费还是失败的情况下;

3 代码实现MQ延迟队列实现订单超时

数据库建表

create table order_info(
	id int primary key auto_increment,
	order_name varchar(20),
	order_status int default 0, -- 0待支付 -- 1已支付 -- 2订单超时
	order_id varchar(50)
);

配置文件

spring:
  rabbitmq:
    host: 39.105.140.192
    username: root
    password: root
    virtual-host: /

  datasource:
    url: jdbc:mysql://localhost:3306/rabbitmq?useUnicode=true&characterEncoding=UTF-8&&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    #开启下划线转驼峰
    mapUnderscoreToCamelCase: true

server:
  port: 8080

核心代码

package com.chenyun.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
    private int id;
    private String orderName;
    private int orderStatus;
    private String orderId;

    public Order(String orderName, String orderId) {
        this.orderName = orderName;
        this.orderId = orderId;
    }
}
package com.chenyun.mapper;

import com.chenyun.pojo.Order;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;

@Repository
public interface OrderMapper {
    @Insert("insert into order_info(order_name,order_id) values(#{orderName},#{orderId})")
    int addOrder(Order order);

    @Select("SELECT * from order_info where order_id=#{orderId} ")
    Order getOrder(String orderId);

    @Update("update order_info set order_status=#{orderStatus} where order_id=#{orderId};")
    int updateOrderStatus(@Param("orderId") String orderId,@Param("orderStatus") Integer orderStatus);

}
package com.chenyun.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class DeadLetterMQConfig {
    /**
     * 订单交换机
     */
    private String orderExchange = "order_exchange";

    /**
     * 订单队列
     */
    private String orderQueue = "order_queue";

    /**
     * 订单路由key
     */
    private String orderRoutingKey = "order_routingKey";

    /**
     * 死信交换机
     */
    private String dlxExchange = "order_dlx_exchange";

    /**
     * 死信队列
     */
    private String dlxQueue = "order_dlx_queue";
    /**
     * 死信路由
     */
    private String dlxRoutingKey = "order_dlx_routingKey";

    /**
     * 声明死信交换机
     *
     * @return DirectExchange
     */
    @Bean
    public DirectExchange dlxExchange() {
        return new DirectExchange(dlxExchange);
    }

    /**
     * 声明死信队列
     *
     * @return Queue
     */
    @Bean
    public Queue dlxQueue() {
        return new Queue(dlxQueue);
    }

    /**
     * 声明订单业务交换机
     *
     * @return DirectExchange
     */
    @Bean
    public DirectExchange orderExchange() {
        return new DirectExchange(orderExchange);
    }

    /**
     * 声明订单队列 TTL(延迟,并绑定死信交换机)
     *
     * @return Queue
     */
    @Bean
    public Queue orderQueue() {
        Map<String, Object> arguments = new HashMap<>(2);
        // 绑定死信交换机
        arguments.put("x-dead-letter-exchange", dlxExchange);// 指定时期后消息投递给哪个交换器。
        // 绑定死信路由key
        arguments.put("x-dead-letter-routing-key", dlxRoutingKey);// 指定到期后投递消息时以哪个路由键进行投递
        arguments.put("x-message-ttl", 20000);                         // 指定到期时间。5 秒
        return new Queue(orderQueue, true, false, false, arguments);
    }

    /**
     * 绑定订单队列到订单交换机
     *
     * @return Binding
     */
    @Bean
    public Binding orderBinding() {
        return BindingBuilder.bind(orderQueue())
                .to(orderExchange())
                .with(orderRoutingKey);
    }
    /**
     * 绑定死信队列到死信交换机
     *
     * @return Binding
     */
    @Bean
    public Binding binding() {
        return BindingBuilder.bind(dlxQueue())
                .to(dlxExchange())
                .with(dlxRoutingKey);
    }

}
package com.chenyun.web;

import com.chenyun.mapper.OrderMapper;
import com.chenyun.pojo.Order;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DeadLetterController {

    @Autowired
    private RabbitTemplate rabbitTemplate;
    /**
     * 订单交换机
     */
    private String orderExchange = "order_exchange";
    /**
     * 订单路由key
     */
    private String orderRoutingKey = "order_routingKey";

    @Autowired
    private OrderMapper orderMapper;

    /**
     * 生成订单--生产消息
     * @return
     * @throws JsonProcessingException
     */
    @RequestMapping("/sendOrderMsg")
    public String sendOrderMsg() throws JsonProcessingException {
        String orderId = System.currentTimeMillis() + "";
        String orderName = "rabbitmq确认消息被消费";
        Order order = new Order(orderName, orderId);
        int result = orderMapper.addOrder(order);
        if (result <= 0) {
            return "fail";
        }

        sendMsg(orderId);
        return orderId;
    }

    // 如果用同步可以使用消息确认机制,必须投递成功再返回orderId,效率低
    @Async
    public void sendMsg(String orderId) {
        rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, orderId);
        // 消息投递失败
    }

}
package com.chenyun.customer;

import com.chenyun.mapper.OrderMapper;
import com.chenyun.pojo.Order;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Component
public class OrderDlxConsumer {

    @Autowired
    private OrderMapper orderMapper;

    /**
     * 监听死信队列
     *
     * @return
     */
    @RabbitListener(queues = "order_dlx_queue")
    public void orderConsumer(String orderId) {
        System.out.println("死信队列获取消息:" + orderId);
        if (StringUtils.isEmpty(orderId)) {
            return;
        }
        Order order = orderMapper.getOrder(orderId);
        if(order == null){
            return;
        }

        Integer orderStatus = order.getOrderStatus();
        if(orderStatus == 0){
            orderMapper.updateOrderStatus(orderId,2);
            // 对库存实现加1
        }
    }
}

运行结果:

image-20211117201930938

14 消费端限流

消费端每次从队列中取一部分消息,然后消费者解决完业务处理,当业务处理完之后,消费者采用手动应答的方式,回应消息队列,然后继续取一部分消息处理,实现削峰填谷的效果,如下图:多个生产者同时给MQ发送消息10000万,如果不做消费端限流,那么A系统请求瞬间增多 。限流就是让A系统每次从MQ取1000条,然后做业务处理,当处理完后,手动应答队列,然后队列在发1000条处理,反复10次即可处理完请求。

image-20220209140511866

注意点:在rabbit:listener-container 中配置 prefetch属性设置消费端一次拉取多少消息,消费端的确认模式一定为手动确认。acknowledge=“manual”

消费者端配置:

spring:
    application:
        name: consumer2022_1
    rabbitmq:
        host: 39.105.140.192
        username: root
        password: root
        virtual-host: /
        listener:
          simple:
              acknowledge-mode: manual
              prefetch: 2

消息监听器

需要注意的是,代码中我故意屏蔽了手动确认的方法,导致消费者端在接收到消息后始终无法确认,这样就会阻塞消费者端继续从MQ获取消息,达到验证限流的目的

package com.example.consumer;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class SmsConsumer {

    @RabbitListener(queues = "sms_queue")
    public void smsConsumer(Message message, Channel channel) throws IOException, InterruptedException {
        Thread.sleep(1000);
        System.out.println("*********************************************************");
        byte[] body = message.getBody();
        String msg = new String(body);
        System.out.println("订单队列获取消息:"+msg);
//        channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);
    }
}

消息生产者:

连续发送10条消息

    @Test
    void contextLoads() {

        for (int i=0;i<10;i++){
            rabbitTemplate.convertAndSend("direct-exchange-1","sms","hello"+i);
        }
    }

执行结果:

image-20220209140827906

可以清楚的看到,消费者由于配置了prefetch: 2并且接收代码中没有消息确认,剩下队列中的8条消息,始终无法被获取到,达到了限流的目的。

15 延迟队列

延迟队列,即消息进入队列后不会立即被消费者调用,只有到达指定时间后,才会被调用者调用消费。

需求:

1、下单后,30分钟未支付,取消订单,回滚库存。

2、新用户注册成功7天后,发送短信问候。

很遗憾,在RabbitMQ中并未提供延迟队列功能。但是可以使用:TTL+死信队列 组合实现延迟队列的效果,如下图.

image-20220209142422784

16 基于RabbitMQ 的最终一致性分布式事务

1、在实际开发中,有三个服务A、B、C,它们各自要执行一条sql操作,如果这个时候是串行调用,也就是说,A操作一条sql后调用B服务,B操作sql后调用C服务,如果C操作出现异常(操作失败),怎么办?

如果A通过openfeign调用B,B通过openFeign调用C,C操作失败,那么C操作回滚,返回一个状态给openfeign,则B收到该状态也执行回滚操作,也返回一个状态给A的openfeign,A收到这个状态之后也可以执行回滚,则保证了它们事务的一致性。即在它们服务对应的操作方法上添加@Transactional(rollbackfor)默认运行时异常回滚。

2、问题2:如果A服务 同时调用B服务和C服务,不再是串行,而是并行调用B和C。B和C之间没有调用关系。如果A操作成功,A先调用B服务,B服务也成功,然后A服务调用C服务,如果C服务失败,那么可以回滚A服务,那么B服务怎么回滚呢?怎么办呢?

1. 整体思路

案例:注册新用户后,可以慢慢等待促销中心为新用户发电子优惠券,并非强制要求同时性。

  1. 可靠生产 : 保证消息一定要发送到 RabitMQ 服务。
  2. 可靠消费 : 保证消息取出来一定正确消费掉。

最终使多方数据达到一致

2. 简单方案

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qch42qPg-1644564430559)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220211114353626.png)]

A注册用户保存到本地数据库用户表,同时写入消息到数据库消息表,定时器定期查询消息表的某些消息(消息状态为“未发送”),然后把消息发送到消息队列,队列收到消息回调确认,同时修改消息表的消息状态(已发送),促销服务监听队列接收消息,发放优惠券,发送成功后才手动应答,然后队列删除该消息

2.1 生产方
1 生产方『可靠性』

这里的『可靠性』指的是一旦 A 服务(事务的发起方)本地操作执行成功后,要务必确保消息一定要发送至 RabbitMQ 。

如果发送失败,那么:

  1. 撤销 A 服务的本地操作;
  2. 如果 A 服务的本地操作是无法撤销的,那么消息需要重发;如果重复仍然失败,那么则需要人工干预。发送消息到rabbitmq由定时器来完成,
2 表

用户表,可以只需要3个字段

CREATE TABLE `userinfo`  (
  `id` int(0) NOT NULL AUTO_INCREMENT,
  `username` varchar(15) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `password` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

消息表

CREATE TABLE `message`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `exchange` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `routing_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `message_content` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `status` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `retry_count` int(0) NOT NULL DEFAULT 3,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

其中 retry_count 是配合定时任务实现消息重发,极简情况下,这个字段可以没有,那么整个功能就更简单一些。status 表示消息状态(已发送和未发送两种)

生产者服务做完本身的业务操作后,要向消息表中添加一条记录,表示有一条待发送消息。注意,生产者服务的本身的操作和向消息表中添加记录这两个操作要在同一个事务中。

消息去重表

CREATE TABLE `reliable`  (
  `id` int(0) NOT NULL AUTO_INCREMENT,
  `userid` int(0) DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `userid`(`userid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

创建 工程 : mq-transaction-consistency-register 注册用户,发送消息

3 核心代码

application.yml配置

mybatis:
    mapper-locations: classpath:mappers/*xml
spring:
    application:
        name: mq-transaction-consistency-register
    datasource:
        url: jdbc:mysql://localhost:3306/rabbitmq_tx?useUnicode=true&characterEncoding=UTF-8&&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
    rabbitmq:
        host: 39.105.140.192
        username: root
        password: root
        virtual-host: /
        ###开启消息确认机制
        publisher-confirm-type: CORRELATED
        publisher-returns: true

定义交换机和消息队列,以及交换机和队列的绑定

package com.example.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitmqConfig {

    public static String exchangeName = "message_exchange";
    public static String queueName = "message_queue";

    /**
     *声明message_queue队列
     */
    @Bean
    public Queue msgQueue(){
        return new Queue(queueName);
    }

    /**
     *声明直连交换机message_exchange
     */
    @Bean
    public TopicExchange topicExchange(){
        return new TopicExchange(exchangeName);
    }

    /**
     * 将message_queue和message_exchange建立绑定关系
     * @param msgQueue 队列
     * @param topicExchange 交换机
     * @return
     */
    @Bean
    public Binding binding1(Queue msgQueue, TopicExchange topicExchange) {
        return BindingBuilder.bind(msgQueue).to(topicExchange).with("user.#");
    }
}

业务层:保存用户和消息

package com.example.service.impl;

import com.example.config.RabbitmqConfig;
import com.example.dao.MessageMapper;
import com.example.dao.UserMapper;
import com.example.entity.Message;
import com.example.entity.User;
import com.example.service.UserService;
import com.example.util.MessageStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private MessageMapper messageMapper;

    @Override
    public void add(User user) {
        userMapper.insert(user);

        Message message = new Message();
        message.setRetryCount(10);
        message.setStatus(MessageStatus.UNSEND.getMessage());
        message.setMessageContent(user.getId().toString());
        message.setExchange(RabbitmqConfig.exchangeName);
        message.setRoutingKey("user.register");

        messageMapper.insert(message);
    }
}

定时器定期发送消息

package com.example.timer;

import com.example.config.RabbitmqConfig;
import com.example.dao.MessageMapper;
import com.example.entity.Message;
import com.example.entity.MessageExample;
import com.example.util.MessageStatus;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@Slf4j
public class MessageSendTimer {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private MessageMapper messageMapper;

    @Scheduled(fixedRate = 6000) //每6秒调用这个方法
    public void sendMessage(){

        log.info("定时器 开始工作");
        MessageExample messageExample = new MessageExample();
        MessageExample.Criteria criteria = messageExample.createCriteria();
        //查询未发生,且重试次数大于0
        criteria.andStatusEqualTo(MessageStatus.UNSEND.getMessage());//状态为 未发送 的消息
        criteria.andRetryCountGreaterThan(0);//重试次数大于0
        List<Message> messages = messageMapper.selectByExample(messageExample);
        log.info("共有{}条消息等待发送",messages.size());
        log.info("============================================");

        //遍历集合,发送消息
        for(Message item:messages){
            //消息id
            log.debug("发送消息[{}]", item.getId());

            CorrelationData data = new CorrelationData(item.getId().toString());
            rabbitTemplate.convertAndSend(item.getExchange(),item.getRoutingKey(),item.getMessageContent(),data);
            //改变该消息的重试次数 减去一次
            item.setRetryCount(item.getRetryCount()-1);
            messageMapper.updateByPrimaryKeySelective(item);
            log.info("该消息的重试次数还有{}",item.getRetryCount());
        }
    }
}

回调方法

package com.example.config;

import com.example.dao.MessageMapper;
import com.example.entity.Message;
import com.example.util.MessageStatus;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@Slf4j
public class RabbitMqCallBackConfig {

    @Autowired
    private MessageMapper messageMapper;

    @Bean
    public RabbitTemplate getRabbitTemplate(ConnectionFactory connectionFactory){
        RabbitTemplate rabbitTemplate = new RabbitTemplate();
        rabbitTemplate.setConnectionFactory(connectionFactory);
        // 设置开启 Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
        rabbitTemplate.setMandatory(true);

        // 确认消息已发送到交换机(Exchange)
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                String data = correlationData.getId();
                if(ack){
                    log.debug("消息[{}]成功发送至 Exchange,将改变其状态至【发送成功】", data);
                    Message message = new Message();
                    message.setId(Long.parseLong(data));
                    message.setStatus(MessageStatus.SENDEN.getMessage());
                    messageMapper.updateByPrimaryKeySelective(message);
                }else{
                    log.info("发送失败");
                }
            }
        });
        return rabbitTemplate;
    }
}

生产方启动类

package com.example;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@MapperScan("com.example.dao")
@EnableTransactionManagement
@EnableScheduling
public class MqTransactionConsistencyRegisterApplication {

    public static void main(String[] args) {
        SpringApplication.run(MqTransactionConsistencyRegisterApplication.class, args);
    }
}
2.2 消费方

17 RabbitMQ应用问题

消息可靠性保障、消息幂等性处理 、微服务中用消息队列实现微服务的异步调用,而用openfeign采用的同步

1 消息可靠性保障-消息补偿

  • 消息补偿机制

需求:
100%确保消息发送成功

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VV6dPzCq-1644564430563)(E:\四阶段\RabbitMQ\讲义\assets\image-20210318121207557.png)]

2 消息幂等性保障-乐观锁(了解)

幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。MQ中指,消费多条相同的消息,得到与消费该消息一次相同的结果。

  • 乐观锁解决方案

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XCYWx3Nz-1644564430564)(E:\四阶段\RabbitMQ\讲义\assets\image-20210318121902195.png)]

第一次生产者发送一条消息,但是消费方系统宕机,即不能立即消费,于是回调检查服务监听不到Q2的响应消息,也不会写入数据库MDB,当隔一段时间后,生产者又发送一条延迟消息到Q3队列,回调检查服务能监听到Q3队列消息,于是和MDB去比较是否有,由于消费方的失败,消息最终没有入库MDB,这个时候回调检查服务和MDB数据库比对失败,于是通知生产者,重新发送一条消息给消费者,那么这个时候Q1就有2条消息了,当消费方正常运行的时候,由于监听的Q1是两条2消息,怎么办呢?乐观锁

第一次执行:version=1
update account set money = money - 500 , version = version + 1
where id = 1 and version = 1

第二次执行:version=2
update account set money = money - 500 , version = version + 1
where id = 1 and version = 1

问题:

大量消息在mq里积压了几个小时了还没解决,怎么办?

这种时候只好采用 “丢弃+批量重导” 的方式来解决了,临时写个程序,连接到mq里面消费数据,收到消息之后直接将其丢弃,快速消费掉积压的消息,降低MQ的压力。或者多启几个消费端

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值