消息队列—RabbitMQ(万字详解)

这篇具有很好参考价值的文章主要介绍了消息队列—RabbitMQ(万字详解)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

消息队列—RabbitMQ

笔记整理自 【涛哥】最适合小白入门的RabbitMQ教程

1. 消息队列介绍

Ⅰ. 消息队列需求场景

在基于微服务开发的电商项目中,商品的查询和商品的修改是通过两个服务实现的,如果修改了商品的价格,如何保证商品查询服务查询出来的商品价格同步更新呢?

服务与服务之间的通信方式有两种:同步调用异步调用

  • 同步调用

    ➢ A服务调用B服务,需要等待B服务执行完毕的返回值,A服务才可以继续往下执行。

    ➢ 通过远程过程调用:REST(Ribbon、Feign)RPC(Dubbo) 实现同步调用。

  • 异步调用

    ➢ A服务调用B服务,而无需等待B服务的执行结果,也就是说在B服务执行的同时A服务可以继续往下执行。

    ➢ 通过:消息队列 实现异步调用。

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  1. 为了保证数据的一致性,当 “商户商品修改服务” 在完成对A库中商品信息的修改之后,需要调用 “商户商品查询服务” 及 “自媒体商品查询服务” 同步完成B库及C库中商品信息的修改;
  2. 如果 “商户商品修改服务” 使用Ribbon或者Feign同步调用 “商户商品查询服务” 及 “自媒体商品查询服务” 虽然能够实现数据的同步修改,但是大大增加了 “商户商品修改服务” 对用户的响应时间
  3. 为了缩短 “商户商品修改服务” 对用户的响应时间,我们可以在 “商户商品修改服务” 完成对A库修改之后,通过异步消息队列通知 “商户商品查询服务” 及 “自媒体商品查询服务”。

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

Ⅱ. 消息队列概念

  • MQ全称为Message Queue,消息队列(MQ)是一种应用程序对应用程序的通信方法。应用程序通过读写出入队列的消息(针对应用程序的数据)来通信,而无需专用连接来链接它们。
  • 消息传递指的是程序之间通过在消息中发送数据进行通信,而不是通过直接调用彼此来通信,直接调用通常是用于诸如远程过程调用的技术。排队指的是应用程序通过队列来通信。队列的使用除去了接收和发送应用程序同时执行的要求。

Ⅲ. 常见消息队列中间件

  • RabbitMQ、ActiveMQ、RocketMQ、Kafka

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • RabbitMQ 稳定可靠,数据一致,支持多协议,有消息确认,基于Erlang语言(专门处理高并发的语言)。

  • ActiveMQ 不够灵活轻巧,对队列较多情况支持不好。

  • RocketMQ 性能好,高吞吐,高可用性,支持大规模分布式,协议支持单一。

  • Kafka 高吞吐,高性能,快速持久化,无消息确认,无消息遗漏,可能会有有重复消息,依赖于zookeeper,成本高。

2. 消息队列作用

Ⅰ. 解耦

场景说明:用户下单后,订单系统需要通知库存系统。

  • 传统做法

    ➢ 传统的做法是,订单系统调用库存系统的接口。如下图:

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    ➢ 传统模式的缺点:假如库存系统无法访问,则订单减库存将失败,从而导致订单失败,订单系统与库存系统耦合。

    ➢ 如何解决以上问题呢?

  • 使用消息队列

    ➢ 引入应用消息队列后的方案,如下图:

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    ➢ 订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。

    ➢ 库存系统:订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作。

    ➢ 在下单时库存系统不能正常使用。也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦。

Ⅱ. 异步

场景说明:用户注册后,需要发注册邮件和注册短信。

  • 传统做法

    ➢ a.串行:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    ➢ b.并行:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • 使用消息队列

    ➢ 将不是必须的业务逻辑,异步处理。改造后的架构如下:

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    这就是RabbitMQ工作模式中的订阅模式(通过交换机广播发送消息到所有队列中)。

Ⅲ. 流量削峰

场景说明:商品秒杀业务,一般会因为流量过大,导致流量暴增,应用挂掉。

  • 传统做法

    ➢ 限制用户数量

  • 使用消息队列

    ➢ 大量的请求不会主动请求秒杀业务,而是存放在消息队列(缓存)。

    ➢ 用户的请求,服务器接收后,首先写入消息队列,秒杀业务根据消息队列中的请求信息,再做后续处理。

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    ➢ 假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面。

Ⅳ. 消息通讯

消息通讯是指,消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等。

  • 使用消息队列实现点对点通信

    ➢ 客户端A和客户端B使用同一队列,进行消息通讯。

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • 使用消息队列实现聊天室通信

    ➢ 客户端A,客户端B,客户端N订阅同一主题,进行消息发布和接收。实现类似聊天室效果。

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

Ⅴ. 日志处理

日志处理是指将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题。

  • 使用消息队列完成日志处理

    ➢ 日志采集客户端,负责日志数据采集,定时写受写入Kafka队列。

    ➢ Kafka消息队列,负责日志数据的接收,存储和转发。

    ➢ 日志处理应用:订阅并消费kafka队列中的日志数据。

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

3. RabbitMQ介绍

Ⅰ. RabbitMQ概述

  • RabbitMQ是一个在AMQP基础上完成的,可复用的企业消息系统。它遵循Mozilla Public License开源协议。

  • AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有RabbitMQ等。

  • 主要特性

    ➢ 保证可靠性:使用一些机制来保证可靠性,如持久化、传输确认、发布确认

    ➢ 灵活的路由功能

    ➢ 可伸缩性:支持消息集群,多台RabbitMQ服务器可以组成一个集群

    ➢ 高可用性:RabbitMQ集群中的某个节点出现问题时队列仍然可用

    ➢ 支持多种协议

    ➢ 支持多语言客户端

    ➢ 提供良好的管理界面

    ➢ 提供跟踪机制:如果消息出现异常,可以通过跟踪机制分析异常原因

    ➢ 提供插件机制:可通过插件进行多方面扩展

Ⅱ. RabbitMQ逻辑结构

RabbitMQ中的逻辑结构,可以类比MySQL:

  • virtual host => database
  • queue => table

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

4. RabbitMQ安装及配置

基于 CentOS 安装 RabbitMQ 3.7

说明:推荐使用本地安装,减少网络依赖。

Ⅰ. 安装前准备

  • 如果之前安装过erlang,先删除

    yum remove erlang*
    
  • 安装C++编译环境

    # yum -y install make gcc gcc-c++
    yum -y install make gcc gcc-c++ kernel-devel m4 ncurses-devel openssl-devel unixODBC unixODBC-devel httpd python-simplejson
    
  • 下载erlang和rabbitMQ

    # 下载erlang
    wget http://www.erlang.org/download/otp_src_20.1.tar.gz
    
    # 下载rabbitMQ
    wget https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.7.0/rabbitmq-server-generic-unix-3.7.0.tar.xz
    

Ⅱ. 安装erlang

  • 解压erlang安装包

    tar -xvf otp_src_20.1.tar.gz
    
  • 进入解压文件夹

    cd otp_src_20.1
    
  • 指定安装目录及安装配置(需要先安装并配置JDK)

    # erlang指定安装在/usr/local/erlang目录 
    ./configure --prefix=/usr/local/erlang --enable-smp-support --enable-threads --enable-sctp --enable-kernel-poll --enable-hipe --with-ssl --without-javac
    
  • 编译与安装

    make && make install
    
  • 配置erlang环境变量

    vi /etc/profile
    

    ➢ 将 export PATH=$PATH:/usr/local/erlang/bin 添加到文件末尾

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • 重新加载profile文件

    source /etc/profile
    

Ⅲ. 安装RabbitMQ

  • 解压RabbitMQ安装包

    ➢ 由于下载的安装包为xz文件,先将xz解压为tar

    xz -d rabbitmq-server-generic-unix-3.7.0.tar.xz
    

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    ➢ 再解压缩tar文件

    tar -xvf rabbitmq-server-generic-unix-3.7.0.tar
    
  • 启动RabbitMQ

    ➢ 进入到解压的RabbitMQ的sbin目录

    cd rabbitmq_server-3.7.0/sbin
    

    ➢ 启动

    ./rabbitmq-server -detached
    

    ➢ 查看进程

    ps aux|grep rabbit
    #ps a 显示现行终端机下的所有程序,包括其他用户的程序。
    #ps u   以用户为主的格式来显示程序状况。
    #ps x   显示所有程序,不以终端机来区分。
    

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

Ⅳ. 启动管理界面

  • 启动RabbitMQ的管理系统插件(需进入sbin目录)

    ./rabbitmq-plugins enable rabbitmq_management
    

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    RabbitMQ的端口是5672,RabbitMQ管理系统的端口是15672。

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

Ⅴ. 放行端口

如果没有网络指令需要先安装:yum install net-tools

  • 查看并放行端口

    netstat -tlnp
    firewall-cmd --add-port=15672/tcp --permanent
    firewall-cmd --add-port=5672/tcp --permanent
    

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • 也可以直接关闭防火墙

    ➢ CentOS7

    #关闭防火墙 
    systemctl stop firewalld
    #开机禁用 
    systemctl disable firewalld
    #查看状态
    systemctl status firewalld
    

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    ➢ CentOS6

    #1.永久性生效,重启后不会复原
    #开启: 
    chkconfig iptables on
    #关闭: 
    chkconfig iptables off
    
    #2.即时生效,重启后复原
    #开启: 
    service iptables start
    #关闭: 
    service iptables stop
    
    #3.查询TCP连接情况:
    netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
    
    #4.查询端口占用情况:
    netstat   -anp   |   grep  portno(例如:netstat –apn | grep 80)
    
  • 云服务器需要在控制台添加 “安全组设置”

    ➢ 阿里云服务器

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    ➢ 华为云服务器

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • 访问页面

    http://192.168.232.102:15672/

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

5. RabbitMQ管理

用户管理

Ⅰ. 用户级别

  • 超级管理员administrator:可以登录控制台,查看所有信息,可以对用户和策略进行操作;
  • 监控者monitoring:可以登录控制台,可以查看节点的相关信息,比如进程数,内存磁盘使用情况;
  • 策略制定者policymaker:可以登录控制台,制定策略,但是无法查看节点信息;
  • 普通管理员management:仅能登录控制台;
  • 其他:无法登录控制台,一般指的是提供者和消费者。

Ⅱ. 添加用户(命令模式)

  • 在Linux中使用命令行创建用户

    # 进入到rabbitmy的sbin目录
    cd /usr/local/rabbitmq_server-3.7.0/sbin
    
  • 添加/配置用户

    # rabbitmqctl中的命令
    ./rabbitmqctl add_user ytao admin123
    
  • 设置用户权限

    # 设置admin为administrator级别
    ./rabbitmqctl set_user_tags ytao administrator
    

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

Ⅲ. 添加用户(web方式)

  • 浏览器访问:http://47.96.11.185:15672/(使用guest guest 登录,guest 具有最高权限,只能在本机登录;先使用命令行创建一个用户)

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • 添加用户

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • 删除用户

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • 为用户分配可以访问的虚拟主机

    ➢ 默认情况下没有任何可以访问的,我们可以添加一个主机(相当于添加一个数据库),然后分配权限

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    ➢ 创建虚拟主机

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    ➢ 给指定用户分配虚拟主机

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    ➢ 设置完成后,回到用户界面确认

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

6. RabbitMQ工作模式

RabbitMQ提供了多种消息的通信方式—工作模式

参考文档:http://www.rabbitmq.com/getstarted.html

消息通信是由两个角色完成:

  • 消息生产者(Producer => P)
  • 消息消费者(Comsumer => C)

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

工作模式就是消息生产者和消息消费者之间是如何通过队列来进行通信的。

Ⅰ. 简单模式(“Hello World”)

简单模式就是我们的生产者将消息发到队列,消费者从队列中取消息,一条消息对应一个消费者。

消费者相当于一个监听器,一直监听着队列,只要生产者把消息发到队列中,那么消费者就会从队列中得到消息。

可以有多个生产者,但一个队列只有一个消费者

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

Ⅱ. 工作模式(Work queues)

Work模式就是一条消息可以被多个消费者尝试接收,但是最终只能有一个消费者能获取

多个消费者监听一个队列。

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

Ⅲ. 订阅模式(Publish/Subscribe)

一条消息可以被多个消费者同时获取,生产者将消息发送到交换机,消费者将自己对应的队列注册到交换机,交换机广播发送消息,发送消息后所有注册的队列的消费者都可以收到消息。

多个消息队列,每个消息队列有一个消费者监听。

交换机有四种工作模式:

There are a few exchange types available: direct, topic, headers and fanout. We’ll focus on the last one – the fanout.

  • fanout:扇出(分发交换机,给每个队列都发送消息)

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

Ⅳ. 路由模式(Routing)

生产者将消息发送到了type为direct模式的交换机,消费者的队列在将自己绑定到路由的时候会给自己绑定一个key,只有消费者发送对应key格式的消息时候队列才会收到消息。

多个消息队列,每个消息队列有一个消费者监听。

We will use a direct exchange instead.

  • direct:直接(指定转发交换机)

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

Ⅴ. Topics模式

主题模式,通过通配符发给某些队列。

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

Ⅵ. RPC模式

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

Ⅶ. 总结

自定义示意图

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

后两种模式:Topic模式、RPC模式不是本次重点。

7. RabbitMQ交换机和队列管理

  • 创建队列

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    队列参数会影响队列的特性,后续死信队列会讲其中的三个参数。

    Message TTL:消息在队列存活的时间。

  • 创建交换机

    这里如果出现 Access refused 访问被拒绝字样的话,可以给登录的账户赋予权限:

    ./rabbitmqctl set_permissions -p / lyc ".*" ".*" ".*"
    

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • 交换机绑定队列

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

图示

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

8. 普通Maven使用RabbitMQ

Ⅰ. 简单模式

一个生产者和一个消费者。

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

基础配置

  • 创建Maven项目

  • 建两个Module

    producer:生产者

    consumer:消费者

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • 添加依赖

    <!-- 连接RabbitMQ -->
    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>4.10.0</version>
    </dependency>
    <!-- 日志 -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.25</version>
    </dependency>
    <!-- 工具类 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.9</version>
    </dependency>
    
    <!-- 整合到spring项目需要导入此依赖 -->
    <dependency>
        <groupId>org.springframework.amqp</groupId>
        <artifactId>spring-rabbit</artifactId>
        <version>1.7.6.RELEASE</version>
    </dependency>
    
  • 在resources下创建日志配置文件

    log4j.properties

    log4j.rootLogger=DEBUG,A1
    log4j.logger.com.taotao = DEBUG
    log4j.logger.org.mybatis = DEBUG
    log4j.appender.A1=org.apache.log4j.ConsoleAppender
    log4j.appender.A1.layout=org.apache.log4j.PatternLayout
    log4j.appender.A1.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c]-[%p] %m%n
    
  • 创建MQ连接工具类

    package com.qfedu.utils;
    
    import com.rabbitmq.client.Connection;
    import com.rabbitmq.client.ConnectionFactory;
    
    public class ConnectionUtil {
         public static Connection getConnection() throws Exception {
             // 1.创建连接工厂
             ConnectionFactory factory = new ConnectionFactory();
             // 2.设置MQ的连接信息
             // 2.1 设置服务地址
             factory.setHost("47.96.11.185");
             // 2.2 设置端口
             factory.setPort(5672);
             // 2.3 设置账号信息,vhost、用户名、密码
             factory.setVirtualHost("host1");
             factory.setUsername("lyc");
             factory.setPassword("admin123");
             // 3.通过工厂对象获取连接
             Connection connection = factory.newConnection();
             return connection;
         }
    }
    

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

消息生产者

发送消息

producer生产者Module下

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

package com.lyc.mq.service;

import com.qfedu.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

public class SendMsg {

    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection(); // 相当于数据库中的创建连接
        // 从连接中创建通道
        Channel channel = connection.createChannel(); // 相当于数据库中的 statement
        // 消息内容
        String message = "Hello World!";
        // 参数1 交换机,如果直接发送信息到队列,则交换机名称为""
        // 参数2 目标队列名称
        // 参数3 设置当前这条消息的属性(如:设置过期时间为10)一般对消息不设置
        // 参数4 消息的内容
        channel.basicPublish("", "queue1", null, message.getBytes());
        System.out.println("Send Message '" + message + "'");
        // 关闭通道和连接
        channel.close();
        connection.close();
    }
}

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

queue1存储了 1 1 1 条消息:

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

只要没有消费者 队列中的数据就不会丢失。

使用Java代码创建队列:

// 声明(创建)队列,使用Java代码在MQ中新建一个队列,如果存在就不创建,不存在就创建
// 参数1 创建的队列名称
/*
   参数2 durable:队列中的数据是否持久化, 队列的声明默认是存放到内存中的,如果rabbitmq重启会丢失;
   如果想重启之后还存在就要使队列持久化,保存到Erlang自带的Mnesia数据库中,当rabbitmq重启之后会读取该数据库
   true:做持久化 | false:不做持久化,一般消息是即时消费的。
 */
/*
   参数3 exclusive:是否排外的,有两个作用:
   1.当连接关闭时connection.close()该队列是否会自动删除;(true则代表仅为当前连接所有 close后会删除)
   2.该队列是否是私有的private,如果不是排外的,可以使用两个消费者都访问同一个队列,没有任何问题
                             如果是排外的,会对当前队列加锁,其他通道channel是不能访问的,如果强制访问会报异常:
                             com.rabbitmq.client.ShutdownSignalException: channel error;
                             一般等于true的话 用于一个队列只能有一个消费者来消费的场景。
 */
/*
   参数4 autoDelete:是否自动删除,当最后一个消费者断开连接之后队列是否自动被删除
   可以通过RabbitMQ Management,查看某个队列的消费者数量,当consumers = 0时队列就会自动删除(无论队列中是否还有数据)。
 */
// 参数5 Map<String, Object> arguments:设置队列参数属性
channel.queueDeclare("queue7", false, false, false, null);

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

消息消费者

接受消息

consumer消费者Module下

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

package com.lyc.mq.service;

import com.qfedu.utils.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

public class ReceiveMsg {

    public static void main(String[] args) throws Exception {
        // 创建一个新的连接
        Connection connection = ConnectionUtil.getConnection();
        // 创建一个通道
        Channel channel = connection.createChannel();
        
        // DefaultConsumer类实现了Consumer接口,通过传入一个频道,
        // 告诉服务器我们需要那个频道的消息,如果频道中有消息,就会执行回调函数handleDelivery
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) // body就是从队列中获取的数据
                    throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("Customer Received '" + message + "'");
            }
        };
        // 自动回复队列应答 -- RabbitMQ中的消息确认机制
        channel.basicConsume("queue1", true, consumer);
    }
}

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

queue1中没有数据了:

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

Ⅱ. 工作模式

一个生产者和多个消费者,但一条消息只能由一个消费者获取。

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • 创建Maven项目

  • 建三个Module

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • 具体配置跟上面简单模式的一样

  • 发送消息

    producer

    package com.lyc.mq.service;
    
    import com.lyc.mq.utils.ConnectionUtil;
    import com.rabbitmq.client.Channel;
    import com.rabbitmq.client.Connection;
    
    import java.util.Scanner;
    
    public class SendMsg {
    
        public static void main(String[] args) throws Exception {
            System.out.print("请输入消息:");
            Scanner sc = new Scanner(System.in);
            String msg = null;
            while (!(msg = sc.nextLine()).equals("quit")) {
                Connection connection = ConnectionUtil.getConnection();
                Channel channel = connection.createChannel();
    
                channel.basicPublish("", "queue2", null, msg.getBytes());
                System.out.println("Producer Send Message '" + msg + "'");
    
                channel.close();
                connection.close();
            }
        }
    }
    
  • 接受消息

    两个消费者代码没什么区别,只是类名和输出语句不一样,为了区分consumer1/2

    consumer1

    package com.lyc.mq.service;
    
    import com.lyc.mq.utils.ConnectionUtil;
    import com.rabbitmq.client.*;
    
    import java.io.IOException;
    
    public class ReceiveMsg2 {
    
        public static void main(String[] args) throws Exception {
            Connection connection = ConnectionUtil.getConnection();
            Channel channel = connection.createChannel();
            Consumer consumer = new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope,
                                           AMQP.BasicProperties properties, byte[] body)
                        throws IOException {
                    String message = new String(body, "UTF-8");
                    System.out.println("Customer1 Received '" + message + "'");
                }
            };
            // 获取queue2的信息
            channel.basicConsume("queue2", true, consumer);
        }
    }
    

    consumer2

    package com.lyc.mq.service;
    
    import com.lyc.mq.utils.ConnectionUtil;
    import com.rabbitmq.client.*;
    
    import java.io.IOException;
    
    public class ReceiveMsg2 {
    
        public static void main(String[] args) throws Exception {
            Connection connection = ConnectionUtil.getConnection();
            Channel channel = connection.createChannel();
            Consumer consumer = new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope,
                                           AMQP.BasicProperties properties, byte[] body)
                        throws IOException {
                    String message = new String(body, "UTF-8");
                    System.out.println("Customer2 Received '" + message + "'");
                }
            };
            // 获取queue2的信息
            channel.basicConsume("queue2", true, consumer);
        }
    }
    

测试

  • 先启动consumer1,2,再启动producer,观察是哪个消费者会获取到信息:

    ➢ 此时两个消费者都在等待中:

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    producer发送消息:msg1

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    consumer1抢到了:

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    producer发送消息:msg2

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    consumer2抢到了:

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    此时我们猜测,接受消息是轮询接收吗?我们多发几条数据看看:

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    RabbitMQ默认是使用轮询来分发消息的。

    如果消息是轮询分发,如果每个消费者执行消费代码的时长不同,处理速度不同,那么很容易造成执行速度快的消费者会闲置,而执行比较慢的消费者会一直处在高消耗的状态。这样显然是不合理的,为了解决这个问题,我们可以采用不公平分发。参考1,参考2

    原则上来讲多个消费者轮流消费消息,因为这些消费者完成的都是同一功能,做的事是一样的,所以我们才需要工作模式。

Ⅲ. 订阅模式

一个生产者和多个消费者,一个条消息可以被多个消费者获取。

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • 改造工作模式的案例

    consumer1监听queue3

    channel.basicConsume("queue3", true, consumer);
    

    consumer2监听queue4

    channel.basicConsume("queue4", true, consumer);
    

    ➢ 发送者要把消息发送到**交换机ex1**中:

    // 参数1填上交换机名ex1 参数2去掉队列名改为null
    // 当参数1不为null,且参数2为null,则说明这是订阅模式。
    channel.basicPublish("ex1", "", null, msg.getBytes());
    
  • 启动消费者

  • 启动生产者

  • 发送消息:hello world

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    两个消费者都接收到消息了。

Ⅳ. 路由模式

一个生产者和多个消费者,发消息给交换机时需要一个key,通过key指定某个队列接受消息。

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • 改造工作模式的案例

    consumer1监听queue5

    channel.basicConsume("queue5", true, consumer);
    

    consumer2监听queue6

    channel.basicConsume("queue6", true, consumer);
    

    ➢ 发送者要把消息发送到交换机ex2中,并指定一个key

    // 参数1填上交换机名ex2 参数2添加值"a" 如果第1个参数(交换机的名字)为null 那么第2个参数代表队列 反之则代表key
    // 当参数1和参数2都不为null,则说明这是路由模式。
    if (msg.startsWith("a")) {
        channel.basicPublish("ex2", "a", null, msg.getBytes());
    } else if (msg.startsWith("b")) {
        channel.basicPublish("ex2", "b", null, msg.getBytes());
    }
    
  • 启动消费者

  • 启动生产者

  • 发送消息

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    两个消费者分别按照指定的key逻辑接收消息。

代码架构整体图

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

9. SpringBoot整合RabbitMQ

SpringBoot应用可以完成自动配置及依赖注入——可以通过Spring直接提供与MQ的连接对象。

Ⅰ. 消息发送者

  • 创建SpringBoot项目"mq-send-demo",使用Spring Initializr快速启动,导入以下依赖:

    ➢ Lombok

    ➢ Spring Web

    ➢ Thymeleaf

    ➢ Spring for RabbitMQ
    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • 如果是在项目中新增RabbitMQ支持导入以下依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.amqp</groupId>
        <artifactId>spring-rabbit-test</artifactId>
        <scope>test</scope>
    </dependency>
    
  • application.yml配置RabbitMQ服务器连接属性

    server:
      port: 9001
    
    spring:
      application:
        name: producer
      rabbitmq:
        host: 192.168.232.102
        port: 5672
        username: lyc
        password: admin123
        virtual-host: host1
    
  • 配置RabbitMQ创建队列(Quence)

    这里没用到这个,使用的是管理系统创建的队列。

    import org.springframework.amqp.core.*;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class RabbitMQConfiguration {
    
        @Bean
        public Queue queue() {
            return new Queue("wfx-quence");
        }
    
        @Bean
        public Queue fanoutQuence() {
            return new Queue("wfx-fanout-quence");
        }
        /**
         * 声明交换机,fanout 类型
         */
        @Bean
        public FanoutExchange fanoutExchange() {
            FanoutExchange fanoutExchange = new FanoutExchange("fanoutExchange");
            return fanoutExchange;
        }
        /**
         * 将队列和交换机绑定
         */
        @Bean
        public Binding bindingFanoutExchange(Queue fanoutQuence, FanoutExchange fanoutExchange) {
            return BindingBuilder.bind(fanoutQuence).to(fanoutExchange);
        }
    
    
        @Bean
        public Queue directQuence1() {
            return new Queue("wfx-direct-quence1");
        }
        @Bean
        public Queue directQuence2() {
            return new Queue("wfx-direct-quence2");
        }
        /**
         * 声明交换机,direct 类型
         */
        @Bean
        public DirectExchange directExchange() {
            DirectExchange directExchange = new DirectExchange("directExchange");
            return directExchange;
        }
        /**
         * 将队列和交换机绑定
         */
        @Bean
        public Binding bindingDirectExchange(Queue directQuence1, DirectExchange directExchange) {
           return BindingBuilder.bind(directQuence1).to(directExchange).with("rk1");
        }
    
        @Bean
        public Binding bindingDirectExchange2(Queue directQuence2, DirectExchange directExchange) {
            return BindingBuilder.bind(directQuence2).to(directExchange).with("rk2");
        }
    }
    
  • 在消息发送者中注入AmqpTemplate对象即可发送消息

    @Service
    public class SendMsgService {
    
        @Autowired
        private AmqpTemplate amqpTemplate;
    
        public void sendMsg(String message){
            // 1.发送消息到队列
            amqpTemplate.convertAndSend("queue1", message);
    
            // 2.将消息发送到交换机(订阅交换机)
            //amqpTemplate.convertAndSend("fanout", "", message);
    
            // 3.将消息发送到交换机(路由交换机)
            //amqpTemplate.convertAndSend("direct", "rk1", message);
        }
    }
    
  • 实操案例

    Service

    package com.lyc.producer.service;
    
    import org.springframework.amqp.core.AmqpTemplate;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    
    @Service
    public class TestService {
    
        @Resource
        private AmqpTemplate amqpTemplate;
    
        public void sendMsg(String msg) {
            if (msg.startsWith("q_")) {
                // 1.发送消息到队列
                amqpTemplate.convertAndSend("queue1", msg);
            } else if (msg.startsWith("f_")) {
                // 2.将消息发送到交换机(订阅交换机)
                amqpTemplate.convertAndSend("ex1", "", msg);
            } else if (msg.startsWith("r_")) {
                // 3.将消息发送到交换机(路由交换机)
                if (msg.startsWith("r_a")) {
                    amqpTemplate.convertAndSend("ex2", "a", msg);
                } else if (msg.startsWith("r_b")) {
                    amqpTemplate.convertAndSend("ex2", "b", msg);
                }
            }
        }
    }
    

    Controller

    package com.lyc.producer.controller;
    
    import com.lyc.producer.service.TestService;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.annotation.Resource;
    
    @RestController
    public class TestController {
    
        @Resource
        TestService testService;
    
        @RequestMapping("test")
        public String test(String msg) {
            testService.sendMsg(msg);
            return "success";
        }
    }
    
  • 测试

    ➢ 发送消息:q_helloworld(简单模式)

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    此时应该只有queue1收到消息:

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    ➢ 发送消息:f_helloworld(订阅模式)

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    此时queue3、4都应该收到消息:

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    ➢ 发送消息:r_a_helloworld(路由模式)

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    此时queue5应该收到消息:

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    ➢ 发送消息:r_b_helloworld(路由模式)

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    此时queue6应该收到消息:

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

Ⅱ. 消息接收者

  • 配置跟发送者完全一致

    记得把application.yml的端口号改成9002。

  • 监听队列

    @RabbitListener => 标记在类上,监听哪个队列(一般是一个service对应一个队列)

    @RabbitHandler => 标记在方法上,接到的数据传到这个方法中

    @Service
    @RabbitListener(queues = "queue1")
    public class ReceiveMsgService {
    
        @RabbitHandler // 标记当前方法是用来处理消息的
        public void receiveMsg(String message) { // 接受的参数要跟发过来的类型进行匹配 可以是byte[]、Object(转成json字符串)等。
            System.out.println("收到queue1消息: =>" + message);
        }
    }
    

    因为之前queue1中有 1 1 1 条消息,所以consumer启动直接消费:

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    此时会监听所有从queue1中发过来的消息:

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

订阅模式与路由模式配置可参考:Spring cloud 使用 RabbitMq 04 发布与订阅 (路由模式)。

@RabbitListener@Queue的注解参数参考:RabbitMq Queue/RabbitListener注解参数详解

10. 使用RabbitMQ传递对象

  • 本周就是将对象转换成字符串进行传递

Ⅰ. 对象序列化实现

要求

  • bean实现序列化接口
  • 生产者消费者的bean包名、类名、属性名必须一致
  • 消息生产者

    @Service
    public class SendMsgService {
    
        @Autowired
        private AmqpTemplate amqpTemplate;
    
        public void sendMsg(Goods goods) {
            // 消息队列可以发送:字符串(String)、字节数组(byte[])、序列化对象(Serializable)
            amqpTemplate.convertAndSend("queue1", goods);
        }
    }
    
  • 消息消费者

    @Service
    @RabbitListener(queues = "queue1")
    public class ReviceMsgService {
    
        @RabbitHandler
        public void receiveMsg(Goods goods) { // 传的是什么 接收的参数就是什么(String、byte[]、序列化对象)
            System.out.println(goods);
        }
    }
    

Ⅱ. 序列化字节数组实现

本质也是序列化。

  • 上面是通过convertAndSend()方法转换
  • 这个是手动转换
  • 消息生产者

    @Service
    public class SendMsgService {
    
        @Autowired
        private AmqpTemplate amqpTemplate;
    
        public void sendMsg(Goods goods) {
            byte[] bytes = SerializationUtils.serialize(goods);
            // 消息队列可以发送:字符串(String)、字节数组(byte[])、序列化对象(Serializable)
            amqpTemplate.convertAndSend("queue1", bytes);
        }
    }
    
  • 消息消费者

    @Service
    @RabbitListener(queues = "queue1")
    public class ReviceMsgService {
    
        @RabbitHandler
        public void receiveMsg(byte[] bs) { // 传的是什么 接收的参数就是什么(String、byte[]、序列化对象)
            Goods goods = (Goods) SerializationUtils.deserialize(bs);
            System.out.println(goods);
        }
    }
    

Ⅲ. JSON字符串实现

要求

  • 对象的属性名一致
  • 消息生产者

    @Service
    public class SendMsgService {
    
        @Autowired
        private AmqpTemplate amqpTemplate;
    
        public void sendMsg(Goods goods) throws JsonProcessingException {
            ObjectMapper mapper = new ObjectMapper();
            String message = mapper.writeValueAsString(goods);
            amqpTemplate.convertAndSend("queue1", message);
        }
    }
    
  • 消息消费者

    @Service
    @RabbitListener(queues = "queue1")
    public class ReviceMsgService {
    
        @RabbitHandler
        public void receiveMsg(String msg) throws JsonProcessingException {
            ObjectMapper mapper = new ObjectMapper();
            Goods goods = mapper.readValue(msg, Goods.class);
            System.out.println(goods);
        }
    }
    

11. 基于Java的交换机与队列创建

我们使用消息队列,消息队列和交换机可以通过管理系统完成创建,也可以在应用程序中通过Java代码来完成创建。

Ⅰ. 普通Maven项目

  • 队列的创建

    // 1. 声明队列-queue1
    // 参数1:queue - 指定队列的名称
    // 参数2:durable - 当前队列是否需要持久化(true)
    // 参数3:exclusive - 是否排外(conn.close() - 当前队列会被自动删除,当前队列只能被一个消费者消费)
    // 参数4:autoDelete - 如果这个队列没有消费者在消费,队列自动删除
    // 参数5:arguments - 指定当前队列的其他信息
    channel.queueDeclare("queue1", true, false, false, null);
    
  • 交换机的创建

    // 2. 创建exchange
    // 参数1:exchange的名称
    // 参数2:指定exchange的类型  FANOUT - pub/sub , DIRECT - Routing , TOPIC - Topics
    channel.exchangeDeclare("ex3", BuiltinExchangeType.FANOUT);
    channel.exchangeDeclare("ex4", BuiltinExchangeType.DIRECT);
    
  • 交换机绑定队列

    // 3.交换机绑定队列
    // 参数1:队列的名称
    // 参数2:交换机的名称
    // 参数3:如果为""则说明是订阅模式,有值(key)则说明是路由模式
    channel.queueBind("pubsub-queue1", "ex3", "");
    channel.queueBind("pubsub-queue2", "ex4", "a");
    

Ⅱ. SpringBoot配置

  • 配置RabbitMQ创建队列(Quence)

    import org.springframework.amqp.core.*;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class RabbitMQConfiguration {
    
        /**
         * 声明队列
         */
        @Bean
        public Queue queue() {
            Queue queue = new Queue("wfx-quence");
            // 设置队列属性
            // queue.xx()
            return queue;
        }
    
        @Bean
        public Queue fanoutQuence() {
            return new Queue("wfx-fanout-quence");
        }
        /**
         * 声明交换机,fanout(订阅)类型
         */
        @Bean
        public FanoutExchange fanoutExchange() {
            FanoutExchange fanoutExchange = new FanoutExchange("fanoutExchange");
            return fanoutExchange;
        }
        /**
         * 将队列和交换机绑定
         * 参数中的队列和交换机可以通过创建的方法名进行标识
         */
        @Bean
        public Binding bindingFanoutExchange(Queue fanoutQuence, FanoutExchange fanoutExchange) {
            return BindingBuilder.bind(fanoutQuence).to(fanoutExchange);
        }
    
        @Bean
        public Queue directQuence1() {
            return new Queue("wfx-direct-quence1");
        }
        @Bean
        public Queue directQuence2() {
            return new Queue("wfx-direct-quence2");
        }
        /**
         * 声明交换机,direct(路由)类型
         */
        @Bean
        public DirectExchange directExchange() {
            DirectExchange directExchange = new DirectExchange("directExchange");
            return directExchange;
        }
        /**
         * 将队列和交换机绑定
         * 参数中的队列和交换机可以通过创建的方法名进行标识
         */
        @Bean
        public Binding bindingDirectExchange(Queue directQuence1, DirectExchange directExchange) {
            return BindingBuilder.bind(directQuence1).to(directExchange).with("rk1"); // .with():key
        }
    
        @Bean
        public Binding bindingDirectExchange2(Queue directQuence2, DirectExchange directExchange) {
            return BindingBuilder.bind(directQuence2).to(directExchange).with("rk2"); // .with():key
        }
    }
    

一般不使用临时Java代码创建队列,大部分直接使用管理系统创建队列。

12. RabbitMQ的消息可靠性

消息生产者和消息消费者并不是直接通信,而是通过RabbitMQ进行交互,但我们要确保消息生产者发送的消息 能够被消息消费者收到。

所以我们要保证中间件RabbitMQ是可靠的,也就是我们要保证队列的可靠,怎么保证?

  • 确保消息生产者的消息发送到了RabbitMQ
  • 确保RabbitMQ把消息发送到了消息消费者

RabbitMQ提供了两种保障机制,保障消息在RabbitMQ的安全性:

  • 事务
  • 消息确认和return机制

Ⅰ. RabbitMQ事务

事务几乎不用。

RabbitMQ事务非常消耗资源,添加了事务之后,消息处理的效率会降级几十倍甚至上百倍。

// 1.开启事务
channel.txSelect();

// 2.提交事务
channel.txCommit();

// 3.事务回滚
channel.txRollback();

在代码中使用

channel.txSelect(); // 开启事务
try {
    channel.basicPublish("", "queue1", null, msg.getBytes());
    channel.txCommit(); // 提交事务
} catch (Exception e) {
    channel.txRollback(); // 回滚事务
}

Ⅱ. 消息确认和return机制

因为RabbitMQ的事务效率太低,所以RabbitMQ又提供了第二种保障机制:消息确认和return机制

  • 消息确认机制:确认消息生产者是否成功发送消息到交换机
  • return机制:确认消息是否成功的从交换机分发到队列

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

消息确认如果失败,就不会进行return校验;如果消息确认成功,return机制可能成功也有可能失败。

1️⃣ 普通Maven项目的消息确认和return机制
  • 普通confirm方式

    Connection connection = ConnectionUtil.getConnection();
    Channel channel = connection.createChannel();
    String message = "Hello World!";
    
    // 1.发送消息之前开启消息确认
    channel.confirmSelect();
    // 2.发送消息
    channel.basicPublish("ex2", "c", null, message.getBytes());
    // 3.获取确认
    boolean b = channel.waitForConfirms();
    System.out.println("消息发送" + (b ? "成功" : "失败"));
    
    channel.close();
    connection.close();
    
  • 批量confirm方式

    Connection connection = ConnectionUtil.getConnection();
    Channel channel = connection.createChannel();
    String message = "Hello World!";
    
    // 1.开启消息确认
    channel.confirmSelect();
    // 2.发送消息
    for (int i = 1; i <= 10; i++) {
        message += i;
        channel.basicPublish("ex2", "c", null, message.getBytes());
    }
    // 3.批量确认:发送的所有消息中有一个失败就直接全部失败,抛出IO异常
    boolean b = channel.waitForConfirms();
    
    channel.close();
    connection.close();
    
  • 异步confirm方式

    假如上述发消息需要10s,底下的代码waitForConfirms()会进入阻塞状态,不会往下执行;

    但是在某些应用场景 我并不想让它等待,消息发送后继续往下执行,你给我的确认结果晚一点无所谓。

    Connection connection = ConnectionUtil.getConnection();
    Channel channel = connection.createChannel();
    String message = "Hello World!";
    
    // 1.开启消息确认
    channel.confirmSelect();
    // 2.发送消息
    for (int i = 1; i <= 10; i++) {
        message += i;
        channel.basicPublish("ex2", "c", null, message.getBytes());
    }
    
    // 会阻塞
    //boolean b = channel.waitForConfirms();
    
    // 3.开启异步confirm 监听器 相当于启动了另外一个线程专门用于等待消息确认结果(ack) 可以放到发送消息之前
    channel.addConfirmListener(new ConfirmListener() {
        // 参数1:Long l 表示返回的消息标识
        // 参数2:boolean b 表示是否为批量confirm
        public void handleAck(long l, boolean b) throws IOException {
            System.out.println("----消息发送成功");
        }
        public void handleNack(long l, boolean b) throws IOException {
            System.out.println("----消息发送失败");
        }
    });
    
    // 异步confirm就不能关闭连接和通道了 因为代码会继续往下执行
    //channel.close();
    //connection.close();
    
  • return机制

    ➢ 发送消息之前开启return机制

    ➢ 发送消息时指定mandatory参数为true

    ➢ 由于return机制是异步处理,所以在发送消息之后不关闭channel

    Connection connection = ConnectionUtil.getConnection();
    Channel channel = connection.createChannel();
    String message = "Hello World!";
    
    // return机制:监控交换机是否将消息分发到队列
    channel.addReturnListener(new ReturnListener() {
        // 如果交换机分发消息到队列失败 则会执行此方法(用来处理交换机分发消息到队列失败的情况)
        // 参数3:String s1:目标交换机的名称
        // 参数4:String s2:指定队列的key
        public void handleReturn(int i, String s, String s1, String s2, AMQP.BasicProperties basicProperties,
                                 byte[] bytes) throws IOException {
            System.out.println("消息未分发到队列中");
        }
    });
    
    //channel.basicPublish("", "queue1", null, message.getBytes());
    // 因为key="c"不存在 所以执行handleReturn() 参数mandatory要设置为true(开启return机制的业务处理)
    // 如果有这个true,则监听消息确认和return机制 如果没有,则只监听消息确认机制
    channel.basicPublish("ex2", "c", true, null, message.getBytes());
    
    // 同样也不能close()。因为return是异步执行的
    //channel.close();
    //connection.close();
    
  • addConfirmListener()用来监听生产者是否成功发送消息到交换机
  • addReturnListener()用来监听交换机是否成功分发消息到队列
2️⃣ SpringBoot应用消息确认和return机制
  • 配置application.yml

    开启消息确认和return机制(其他配置省略)

    spring:
      rabbitmq:
        publisher-confirm-type: simple # 开启消息确认机制
        publisher-returns: true        # 开启return机制
    
  • 开启confirm和return监听

    实现两个内部类的接口:RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    RabbitTemplateAmqpTemplate接口的实现类。

    package com.qfedu.mq_producer.utils;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.amqp.core.Message;
    import org.springframework.amqp.rabbit.connection.CorrelationData;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.PostConstruct;
    import javax.annotation.Resource;
    
    @Component
    public class PublisherConfireAndReturnConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
    
        Logger logger = LoggerFactory.getLogger(PublisherConfireAndReturnConfig.class);
    
        @Resource
        private RabbitTemplate rabbitTemplate; // Spring提供的即时消息模板
    
        // 声明当前自己这个类 就是RabbitMQ的消息确认机制类以及return机制类
        @PostConstruct
        public void initMethod(){
            rabbitTemplate.setConfirmCallback(this);
            rabbitTemplate.setReturnCallback(this);
        }
    
        // 此方法用于监听消息确认结果(消息是否发送到了交换机)
        @Override
        public void confirm(CorrelationData correlationData, boolean ack, String s) {
            if (ack) {
                logger.info("--------消息发送(到交换机)成功");
            } else {
                logger.warn("--------消息发送(到交换机)失败");
            }
        }
    
        // 此方法用于return监听(当交换机分发消息到队列失败时执行)
        @Override
        public void returnedMessage(Message message, int i, String s, String s1, String s2) {
            logger.info("~~~~~~~~消息发送到交换机但未分发到队列!!!");
        }
    }
    

Ⅲ. 避免消息重复消费

重复消费消息,会对非幂等行操作造成问题。
重复消费消息的原因是,消费者没有给RabbitMQ一个ack。

为了解决消息重复消费的问题,可以采用Redis,在消费者消费消息之前,现将消息的id放到Redis中,

id-0(正在执行业务)

id-1(执行业务成功)

如果ack失败,在RabbitMQ将消息交给其他的消费者时,先执行setnx,如果key已经存在,获取它的值,如果是0,当前消费者就什么都不做,如果是1,直接ack。

极端情况:第一个消费者在执行业务时,出现了死锁,在setnx的基础上,再给key设置一个生存时间。

1️⃣ 普通Maven项目避免重复消费
  • 生产者,发送消息时,指定messageId

    AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
        .deliveryMode(1)     // 指定消息书否需要持久化 1 - 需要持久化  2 - 不需要持久化
        .messageId(UUID.randomUUID().toString())
        .build();
    String msg = "Hello-World!";
    channel.basicPublish("", "HelloWorld", true, properties, msg.getBytes());
    
  • 消费者,在消费消息时,根据具体业务逻辑去操作redis

    DefaultConsumer consume = new DefaultConsumer(channel) {
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            Jedis jedis = new Jedis("192.168.199.109",6379);
            String messageId = properties.getMessageId();
            // 1.setnx到Redis中,默认指定value-0
            String result = jedis.set(messageId, "0", "NX", "EX", 10);
            if (result != null && result.equalsIgnoreCase("OK")) {
                System.out.println("接收到消息:" + new String(body, "UTF-8"));
                // 2.消费成功,set messageId 1
                jedis.set(messageId,"1");
                channel.basicAck(envelope.getDeliveryTag(), false);
            } else {
                // 3.如果1中的setnx失败,获取key对应的value,如果是0,return,如果是1
                String s = jedis.get(messageId);
                if ("1".equalsIgnoreCase(s)) {
                    channel.basicAck(envelope.getDeliveryTag(), false);
                }
            }
        }
    };
    
2️⃣ SpringBoot应用避免重复消费
  • 导入依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  • 编写配置文件

    spring:
      redis:
        host: 47.96.11.185
        port: 6379
    
  • 修改生产者

    @Test
    void contextLoads() throws IOException {
        CorrelationData messageId = new CorrelationData(UUID.randomUUID().toString());
        rabbitTemplate.convertAndSend("boot-topic-exchange", "slow.red.dog", "红色大狼狗!!", messageId);
        System.in.read();
    }
    
  • 修改消费者

    @Autowired
    private StringRedisTemplate redisTemplate;
    
    
    @RabbitListener(queues = "boot-queue")
    public void getMessage(String msg, Channel channel, Message message) throws IOException {
        // 0.获取MessageId
        String messageId = message.getMessageProperties().getHeader("spring_returned_message_correlation");
        // 1.设置key到Redis
        if (redisTemplate.opsForValue().setIfAbsent(messageId, "0", 10, TimeUnit.SECONDS)) {
            // 2.消费消息
            System.out.println("接收到消息:" + msg);
            // 3.设置key的value为1
            redisTemplate.opsForValue().set(messageId,"1", 10, TimeUnit.SECONDS);
            // 4.手动ack
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } else {
            // 5.获取Redis中的value即可 如果是1,手动ack
            if ("1".equalsIgnoreCase(redisTemplate.opsForValue().get(messageId))){
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            }
        }
    }
    

13. RabbitMQ延迟机制(TTL:死信队列)

假设当前有一个场景,A服务负责订单的保存,B服务负责检查订单的支付状态;

如下图所示:

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

B服务需要承担的任务太重了,需要不停的检查订单状态,所以我们可以考虑 让A给B传递信息,使B在30分钟后再来查询支付状态。

此时我们想到使用RabbitMQ来实现服务的交互,但消息的传递是即时的,假如A保存完订单,会直接给B发送消息,此时顾客可能还未支付订单,B就检查订单支付状态为 “未支付”,直接把订单取消了,这是肯定不可以的;

现在我们的目标是:A保存完订单后 立马把订单发送到消息队列中,让消息在队列中停留30分钟,30分钟之后B服务才可以接收到消息,再进行检查:

  • 延迟队列

消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

Ⅰ. 延迟队列

  • 什么是延迟队列

    延迟队列存储的对象肯定是对应的延时消息,所谓”延时消息”是指当消息被发送以后,并不想让消费者立即拿到消息,而是等待指定时间后,消费者才拿到这个消息进行消费。

  • RabbitMQ如何实现延迟队列?

    AMQP协议和RabbitMQ队列本身没有直接支持延迟队列功能,但是可以通过TTL(Time To Live)特性模拟出延迟队列的功能。

  • 消息的TTL(Time To Live)

    消息的TTL就是消息的存活时间。RabbitMQ可以对队列和消息分别设置TTL。对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间。

    ➢ 在创建队列的时候可以设置队列的存活时间,当消息进入到队列并且在存活时间内没有消费者消费,则此消息就会从当前队列被移除;

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    ➢ 创建消息队列没有设置TTL,但是消息设置了TTL,那么当消息的存活时间结束,也会被移除;

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    但是像上图,消息 1 1 1 的超时时间为 10 s 10s 10s,消息 3 3 3 的超时时间为 5 s 5s 5s,这可能会使得消息 3 3 3 的设置失效,因为消息队列也是队列,也必须遵守队列的先进先出的原则,所以队列的消息也会依次消费消息。所以延迟时间的设置一般建议是相等的,或者前小后大也可以。

    当队列设置了超时时间,消息也设置了超时时间,一般以短的为准,谁短谁就生效。

  • 实现延迟队列

    延迟任务通过消息的TTL来实现。我们需要建立2个队列,一个用于发送消息,一个用于消息过期后的转发目标队列。

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

    生产者输出消息到Queue1,并且这个消息是设置有有效时间的,比如60s。消息会在Queue1中等待60s,如果没有消费者收掉的话,它就是被转发到Queue2,Queue2有消费者,收到处理延迟任务。

    没有消费者的队列,而且设置了超时时间,这个队列就称为**死信队列**。

    A服务把消息发送到queue1,但此时的queue1没有消费者 且设置了TTL,所以queue1为死信队列,过了TTL这个时间,就会把消息过期转发到queue2中,而queue2有消费者 B服务,可以即时消费,实现了延迟队列

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

Ⅱ. 创建延迟交换机

  • 创建路由交换机

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • 创建死信队列

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • 创建死信转发队列

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • 交换机队列绑定

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

  • 测试

    ➢ 消息生产者

    消息应该发送到死信队列。

    channel.basicPublish("delay_exchange", "k1", null, msg.getBytes());
    

    ➢ 消息消费者

    由另外一个队列接受消息。

    channel.basicConsume("delay_queue2", true, consumer);
    

    发送消息,通过控制台的日志可以看到是 10 s 10s 10s 后才收到消息:

    消息队列rabbitmq,RabbitMQ,java-rabbitmq,rabbitmq,java,消息队列,中间件

Ⅲ. SpringBoot实现延迟队列

  • 添加MQ依赖

    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.amqp</groupId>
        <artifactId>spring-rabbit-test</artifactId>
        <scope>test</scope>
    </dependency>
    
  • 在application.yml配置RabbitMQ服务器连接属性

    spring:
      application:
        name: mq-sender-demo
      rabbitmq:
        host: 47.96.11.185
        port: 5672
        username: ytao
        password: admin123
        virtual-host: wfx_host
        # 手动ACK 不开启自动ACK模式,目的是防止报错后未正确处理消息丢失 默认 为 none
        listener:
          simple:
            acknowledge-mode: manual
    
  • 生产者

    @Component
    @Slf4j
    public class RabbitProduct{
    
        @Autowired
        private RabbitTemplate rabbitTemplate;
        
        public void sendDelayMessage(List<Integer> list) {
            // 这里的消息可以是任意对象,无需额外配置,直接传即可
            log.info("===============延时队列生产消息====================");
            log.info("发送时间:{},发送内容:{}", LocalDateTime.now(), list.toString());
            this.rabbitTemplate.convertAndSend(
                    "delay_exchange",
                    "delay_key",
                    list,
                    message -> {
                        // 注意这里时间要是字符串形式
                        message.getMessageProperties().setExpiration("60000");
                        return message;
                    }
            );
            log.info("{}ms后执行", 60000);
        }
    }
    
  • 消费者文章来源地址https://www.toymoban.com/news/detail-663239.html

    @Component
    @Slf4j
    public class RabbitConsumer {
        @Autowired
        private CcqCustomerCfgService ccqCustomerCfgService;
    
        /**
         * 默认情况下,如果没有配置手动ACK, 那么 Spring Data AMQP 会在消息消费完毕后自动帮我们去ACK
         * 存在问题:如果报错了,消息不会丢失,但是会无限循环消费,一直报错,如果开启了错误日志很容易就把磁盘空间耗完
         * 解决方案:手动ACK,或者try-catch 然后在 catch 里面将错误的消息转移到其它的系列中去
         * spring.rabbitmq.listener.simple.acknowledge-mode = manual
         * @param list 监听的内容
         */
        @RabbitListener(queues = "receive_queue")
        public void cfgUserReceiveDealy(List<Integer> list, Message message, Channel channel) throws IOException {
            log.info("===============接收队列接收消息====================");
            log.info("接收时间:{},接受内容:{}", LocalDateTime.now(), list.toString());
            // 通知 MQ 消息已被接收,可以ACK(从队列中删除)了
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            try {
                // dosomething...
            } catch (Exception e) {
                log.error("============消费失败,尝试消息补发再次消费!==============");
                log.error(e.getMessage());
                /**
                 * basicRecover方法是进行补发操作,
                 * 其中的参数如果为true是把消息退回到queue但是有可能被其它的consumer(集群)接收到,
                 * 设置为false是只补发给当前的consumer
                 */
                channel.basicRecover(false);
            }
        }
    }
    

到了这里,关于消息队列—RabbitMQ(万字详解)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Java】微服务——RabbitMQ消息队列(SpringAMQP实现五种消息模型)

    微服务间通讯有同步和异步两种方式: 同步通讯:就像打电话,需要实时响应。 异步通讯:就像发邮件,不需要马上回复。 两种方式各有优劣,打电话可以立即得到响应,但是你却不能跟多个人同时通话。发送邮件可以同时与多个人收发邮件,但是往往响应会有延迟。 1.

    2024年02月08日
    浏览(65)
  • Java中如何使用消息队列实现异步(ActiveMQ,RabbitMQ,Kafka)

    在 Java 中,可以使用消息队列实现异步处理。下面是一个简单的示例代码,用于说明如何使用 ActiveMQ 实现消息队列异步处理: 添加 ActiveMQ 依赖 在 pom.xml 文件中添加以下依赖: 创建消息队列 创建一个名为 “TestQueue” 的消息队列,并配置 ActiveMQ 连接信息: 创建消息消费者

    2024年02月16日
    浏览(57)
  • RabbitMQ实现延迟消息,RabbitMQ使用死信队列实现延迟消息,RabbitMQ延时队列插件

    假设有一个业务场景:超过30分钟未付款的订单自动关闭,这个功能应该怎么实现? RabbitMQ使用死信队列,可以实现消息的延迟接收。 队列有一个消息过期属性。就像丰巢超过24小时就收费一样,通过设置这个属性,超过了指定事件的消息将会被丢弃。 这个属性交:x-message

    2024年02月13日
    浏览(79)
  • 【RabbitMQ】消息队列-RabbitMQ篇章

    RabbitMQ是一个开源的 遵循AMQP协议 实现的基于Erlang语言编写,支持多种客户端(语言)。用于在分布式系统中 存储消息,转发消息 ,具有 高可用 , 高可扩性 , 易用性 等特征。 1.1、RabbitMQ—使用场景 一般场景 像一般的下订单业务如下图: 将订单信息写入数据库成功后,发

    2024年02月12日
    浏览(50)
  • 3.精通RabbitMQ—消息队列、RabbitMQ

    RabbitMQ面试题 (总结最全面的面试题) 入门RabbitMQ消息队列,看这篇文章就够了 消息队列 是一种基于 队列 ,用于解决 不同进程或应用 之间 通讯 的 消息中间件 。 支持多种 消息传递模式 ,如 队列模型 、 发布/订阅模型 等。 业务解耦 :通过 发布/订阅 模式,减少系统的 耦

    2024年02月15日
    浏览(79)
  • 【RabbitMQ笔记10】消息队列RabbitMQ之死信队列的介绍

    这篇文章,主要介绍消息队列RabbitMQ之死信队列。 目录 一、RabbitMQ死信队列 1.1、什么是死信队列 1.2、设置过期时间TTL 1.3、配置死信交换机和死信队列(代码配置) (1)设置队列过期时间 (2)设置单条消息过期时间 (3)队列设置死信交换机 (4)配置的基本思路 1.4、配置

    2024年02月16日
    浏览(82)
  • 消息队列-RabbitMQ:延迟队列、rabbitmq 插件方式实现延迟队列、整合SpringBoot

    1、延迟队列概念 延时队列内部是有序的 , 最重要的特性 就体现在它的 延时属性 上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说, 延时队列就是用来存放需要在指定时间被处理的元素的队列。 延迟队列使用场景: 订单在十分钟之内未支付则

    2024年02月22日
    浏览(54)
  • RabbitMq消息模型-队列消息

    基本模型(SimpleQueue)、工作模型(WorkQueue) 队列消息特点: 消息不会丢失 并且 有先进先出的顺序。 消息接收是有顺序的,不是随机的,仅有一个消费者能拿到数据,而且不同消费者拿不到同一份数据。 基本模型: SimpleQueue 在上图的模型中,有以下几个概念: P:为生产

    2024年02月09日
    浏览(49)
  • 消息队列-RabbitMQ:workQueues—工作队列、消息应答机制、RabbitMQ 持久化、不公平分发(能者多劳)

    Work Queues— 工作队列 (又称任务队列) 的主要思想是避免立即执行资源密集型任务,而不得不等待它完成 。 我们把任务封装为消息并将其发送到队列,在后台运行的工作进程将弹出任务并最终执行作业。当有多个工作线程时,这些工作线程将一起处理这些任务 。 轮训分发消

    2024年02月21日
    浏览(56)
  • 【RabbitMQ】RabbitMQ 消息的堆积问题 —— 使用惰性队列解决消息的堆积问题

    消息的堆积问题是指在消息队列系统中,当生产者以较快的速度发送消息,而消费者处理消息的速度较慢,导致消息在队列中积累并达到队列的存储上限。在这种情况下,最早被发送的消息可能会在队列中滞留较长时间,直到超过队列的容量上限。当队列已满且没有更多的可

    2024年02月05日
    浏览(50)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包