笔记整理自 【涛哥】最适合小白入门的RabbitMQ教程
在基于微服务开发的电商项目中,商品的查询和商品的修改是通过两个服务实现的,如果修改了商品的价格,如何保证商品查询服务查询出来的商品价格同步更新呢?
服务与服务之间的通信方式有两种:同步调用 和 异步调用
同步调用
➢ A服务调用B服务,需要等待B服务执行完毕的返回值,A服务才可以继续往下执行。
➢ 通过远程过程调用:REST(Ribbon、Feign)
和 RPC(Dubbo)
实现同步调用。
异步调用
➢ A服务调用B服务,而无需等待B服务的执行结果,也就是说在B服务执行的同时A服务可以继续往下执行。
➢ 通过:消息队列
实现异步调用。
RabbitMQ、ActiveMQ、RocketMQ、Kafka
RabbitMQ 稳定可靠,数据一致,支持多协议,有消息确认,基于Erlang语言(专门处理高并发的语言)。
ActiveMQ 不够灵活轻巧,对队列较多情况支持不好。
RocketMQ 性能好,高吞吐,高可用性,支持大规模分布式,协议支持单一。
Kafka 高吞吐,高性能,快速持久化,无消息确认,无消息遗漏,可能会有有重复消息,依赖于zookeeper,成本高。
场景说明:用户下单后,订单系统需要通知库存系统。
传统做法
➢ 传统的做法是,订单系统调用库存系统的接口。如下图:
➢ 传统模式的缺点:假如库存系统无法访问,则订单减库存将失败,从而导致订单失败,订单系统与库存系统耦合。
➢ 如何解决以上问题呢?
使用消息队列
➢ 引入应用消息队列后的方案,如下图:
➢ 订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。
➢ 库存系统:订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作。
➢ 在下单时库存系统不能正常使用。也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦。
场景说明:用户注册后,需要发注册邮件和注册短信。
传统做法
➢ a.串行:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。
➢ b.并行:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。
使用消息队列
➢ 将不是必须的业务逻辑,异步处理。改造后的架构如下:
这就是RabbitMQ工作模式中的订阅模式(通过交换机广播发送消息到所有队列中)。
场景说明:商品秒杀业务,一般会因为流量过大,导致流量暴增,应用挂掉。
传统做法
➢ 限制用户数量
使用消息队列
➢ 大量的请求不会主动请求秒杀业务,而是存放在消息队列(缓存)。
➢ 用户的请求,服务器接收后,首先写入消息队列,秒杀业务根据消息队列中的请求信息,再做后续处理。
➢ 假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面。
消息通讯是指,消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等。
日志处理是指将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题。
使用消息队列完成日志处理
➢ 日志采集客户端,负责日志数据采集,定时写受写入Kafka队列。
➢ Kafka消息队列,负责日志数据的接收,存储和转发。
➢ 日志处理应用:订阅并消费kafka队列中的日志数据。
RabbitMQ是一个在AMQP基础上完成的,可复用的企业消息系统。它遵循Mozilla Public License开源协议。
AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有RabbitMQ等。
主要特性
➢ 保证可靠性:使用一些机制来保证可靠性,如持久化、传输确认、发布确认
➢ 灵活的路由功能
➢ 可伸缩性:支持消息集群,多台RabbitMQ服务器可以组成一个集群
➢ 高可用性:RabbitMQ集群中的某个节点出现问题时队列仍然可用
➢ 支持多种协议
➢ 支持多语言客户端
➢ 提供良好的管理界面
➢ 提供跟踪机制:如果消息出现异常,可以通过跟踪机制分析异常原因
➢ 提供插件机制:可通过插件进行多方面扩展
RabbitMQ中的逻辑结构,可以类比MySQL:
基于 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安装包
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 添加到文件末尾
重新加载profile文件
source /etc/profile
解压RabbitMQ安装包
➢ 由于下载的安装包为xz文件,先将xz解压为tar
xz -d rabbitmq-server-generic-unix-3.7.0.tar.xz
➢ 再解压缩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的管理系统插件(需进入sbin目录)
./rabbitmq-plugins enable rabbitmq_management
RabbitMQ的端口是5672,RabbitMQ管理系统的端口是15672。
如果没有网络指令需要先安装:yum install net-tools
查看并放行端口
netstat -tlnp
firewall-cmd --add-port=15672/tcp --permanent
firewall-cmd --add-port=5672/tcp --permanent
也可以直接关闭防火墙
➢ CentOS7
#关闭防火墙
systemctl stop firewalld
#开机禁用
systemctl disable firewalld
#查看状态
systemctl status firewalld
➢ 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)
云服务器需要在控制台添加 “安全组设置”
➢ 阿里云服务器
➢ 华为云服务器
访问页面
http://192.168.232.102:15672/
用户管理
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
浏览器访问:http://47.96.11.185:15672/(使用guest guest 登录,guest 具有最高权限,只能在本机登录;先使用命令行创建一个用户)
添加用户
删除用户
为用户分配可以访问的虚拟主机
➢ 默认情况下没有任何可以访问的,我们可以添加一个主机(相当于添加一个数据库),然后分配权限
➢ 创建虚拟主机
➢ 给指定用户分配虚拟主机
➢ 设置完成后,回到用户界面确认
RabbitMQ提供了多种消息的通信方式—工作模式
参考文档:http://www.rabbitmq.com/getstarted.html
消息通信是由两个角色完成:
工作模式就是消息生产者和消息消费者之间是如何通过队列来进行通信的。
简单模式就是我们的生产者将消息发到队列,消费者从队列中取消息,一条消息对应一个消费者。
消费者相当于一个监听器,一直监听着队列,只要生产者把消息发到队列中,那么消费者就会从队列中得到消息。
可以有多个生产者,但一个队列只有一个消费者。
Work模式就是一条消息可以被多个消费者尝试接收,但是最终只能有一个消费者能获取。
多个消费者监听一个队列。
一条消息可以被多个消费者同时获取,生产者将消息发送到
交换机
,消费者将自己对应的队列注册到交换机,交换机广播发送消息,发送消息后所有注册的队列的消费者都可以收到消息。多个消息队列,每个消息队列有一个消费者监听。
交换机有四种工作模式:
There are a few exchange types available: direct, topic, headers and
fanout
. We’ll focus on the last one – thefanout
.
fanout
:扇出(分发交换机,给每个队列都发送消息)
生产者将消息发送到了type为
direct
模式的交换机,消费者的队列在将自己绑定到路由的时候会给自己绑定一个key
,只有消费者发送对应key格式的消息时候队列才会收到消息。多个消息队列,每个消息队列有一个消费者监听。
We will use a
direct
exchange instead.
direct
:直接(指定转发交换机)
主题模式,通过通配符发给某些队列。
自定义示意图
后两种模式:Topic模式、RPC模式不是本次重点。
创建队列
队列参数会影响队列的特性,后续死信队列会讲其中的三个参数。
如Message TTL
:消息在队列存活的时间。
创建交换机
这里如果出现 Access refused
访问被拒绝字样的话,可以给登录的账户赋予权限:
./rabbitmqctl set_permissions -p / lyc ".*" ".*" ".*"
交换机绑定队列
图示
一个生产者和一个消费者。
基础配置
创建Maven项目
建两个Module
➢ producer
:生产者
➢ consumer
:消费者
添加依赖
<dependency>
<groupId>com.rabbitmqgroupId>
<artifactId>amqp-clientartifactId>
<version>4.10.0version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
<version>1.7.25version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.9version>
dependency>
<dependency>
<groupId>org.springframework.amqpgroupId>
<artifactId>spring-rabbitartifactId>
<version>1.7.6.RELEASEversion>
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;
}
}
消息生产者
发送消息
在
producer
生产者Module下
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();
}
}
queue1
存储了 1 1 1 条消息:
只要没有消费者 队列中的数据就不会丢失。
使用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 arguments:设置队列参数属性
channel.queueDeclare("queue7", false, false, false, null);
消息消费者
接受消息
在
consumer
消费者Module下
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);
}
}
queue1
中没有数据了:
一个生产者和多个消费者,但一条消息只能由一个消费者获取。
创建Maven项目
建三个Module
具体配置跟上面简单模式的一样
发送消息
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
,观察是哪个消费者会获取到信息:
➢ 此时两个消费者都在等待中:
➢ producer
发送消息:msg1
➢ consumer1
抢到了:
➢ producer
发送消息:msg2
➢ consumer2
抢到了:
此时我们猜测,接受消息是轮询接收吗?我们多发几条数据看看:
RabbitMQ默认是使用轮询
来分发消息的。
如果消息是轮询分发,如果每个消费者执行消费代码的时长不同,处理速度不同,那么很容易造成执行速度快的消费者会闲置,而执行比较慢的消费者会一直处在高消耗的状态。这样显然是不合理的,为了解决这个问题,我们可以采用不公平分发
。参考1,参考2
原则上来讲多个消费者轮流消费消息,因为这些消费者完成的都是同一功能,做的事是一样的,所以我们才需要工作模式。
一个生产者和多个消费者,一个条消息可以被多个消费者获取。
改造工作模式的案例
➢ 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
两个消费者都接收到消息了。
一个生产者和多个消费者,发消息给交换机时需要一个
key
,通过key
指定某个队列接受消息。
改造工作模式的案例
➢ 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());
}
启动消费者
启动生产者
发送消息
两个消费者分别按照指定的key
逻辑接收消息。
代码架构整体图
SpringBoot应用可以完成自动配置及依赖注入——可以通过Spring直接提供与MQ的连接对象。
创建SpringBoot项目"mq-send-demo
",使用Spring Initializr快速启动,导入以下依赖:
➢ Lombok
➢ Spring Web
➢ Thymeleaf
如果是在项目中新增RabbitMQ支持导入以下依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.amqpgroupId>
<artifactId>spring-rabbit-testartifactId>
<scope>testscope>
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
(简单模式)
此时应该只有queue1
收到消息:
➢ 发送消息:f_helloworld
(订阅模式)
此时queue3、4
都应该收到消息:
➢ 发送消息:r_a_helloworld
(路由模式)
此时queue5
应该收到消息:
➢ 发送消息:r_b_helloworld
(路由模式)
此时queue6
应该收到消息:
配置跟发送者完全一致
记得把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
启动直接消费:
此时会监听所有从queue1
中发过来的消息:
订阅模式与路由模式配置可参考:Spring cloud 使用 RabbitMq 04 发布与订阅 (路由模式)。
@RabbitListener
和@Queue
的注解参数参考:RabbitMq Queue/RabbitListener注解参数详解
要求
- 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);
}
}
要求
- 对象的属性名一致
消息生产者
@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);
}
}
我们使用消息队列,消息队列和交换机可以通过管理系统完成创建,也可以在应用程序中通过Java代码来完成创建。
队列的创建
// 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");
配置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代码创建队列,大部分直接使用管理系统创建队列。
消息生产者和消息消费者并不是直接通信,而是通过RabbitMQ进行交互,但我们要确保消息生产者发送的消息 能够被消息消费者收到。
所以我们要保证中间件RabbitMQ是可靠的,也就是我们要保证队列的可靠,怎么保证?
- 确保消息生产者的消息发送到了RabbitMQ
- 确保RabbitMQ把消息发送到了消息消费者
RabbitMQ提供了两种保障机制,保障消息在RabbitMQ的安全性:
- 事务
- 消息确认和return机制
事务几乎不用。
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(); // 回滚事务
}
因为RabbitMQ的事务效率太低,所以RabbitMQ又提供了第二种保障机制:消息确认和return机制。
- 消息确认机制:确认消息生产者是否成功发送消息到交换机
- return机制:确认消息是否成功的从交换机分发到队列
消息确认如果失败,就不会进行return校验;如果消息确认成功,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()
用来监听交换机是否成功分发消息到队列
配置application.yml
开启消息确认和return机制(其他配置省略)
spring:
rabbitmq:
publisher-confirm-type: simple # 开启消息确认机制
publisher-returns: true # 开启return机制
开启confirm和return监听
实现两个内部类的接口:RabbitTemplate.ConfirmCallback
, RabbitTemplate.ReturnCallback
RabbitTemplate
是AmqpTemplate
接口的实现类。
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设置一个生存时间。
生产者,发送消息时,指定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);
}
}
}
};
导入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
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);
}
}
}
假设当前有一个场景,A服务负责订单的保存,B服务负责检查订单的支付状态;
如下图所示:
B服务需要承担的任务太重了,需要不停的检查订单状态,所以我们可以考虑 让A给B传递信息,使B在30分钟后再来查询支付状态。
此时我们想到使用RabbitMQ来实现服务的交互,但消息的传递是即时
的,假如A保存完订单,会直接给B发送消息,此时顾客可能还未支付订单,B就检查订单支付状态为 “未支付”,直接把订单取消了,这是肯定不可以的;
现在我们的目标是:A保存完订单后 立马把订单发送到消息队列中,让消息在队列中停留30分钟,30分钟之后B服务才可以接收到消息,再进行检查:
延迟队列
什么是延迟队列
延迟队列存储的对象肯定是对应的延时消息,所谓”延时消息”是指当消息被发送以后,并不想让消费者立即拿到消息,而是等待指定时间后,消费者才拿到这个消息进行消费。
RabbitMQ如何实现延迟队列?
AMQP协议和RabbitMQ队列本身没有直接支持延迟队列功能,但是可以通过TTL(Time To Live)特性模拟出延迟队列的功能。
消息的TTL(Time To Live)
消息的TTL就是消息的存活时间。RabbitMQ可以对队列和消息分别设置TTL。对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信
。可以通过设置消息的expiration
字段或者x-message-ttl
属性来设置时间。
➢ 在创建队列的时候可以设置队列的存活时间,当消息进入到队列并且在存活时间内没有消费者消费,则此消息就会从当前队列被移除;
➢ 创建消息队列没有设置TTL,但是消息设置了TTL,那么当消息的存活时间结束,也会被移除;
但是像上图,消息 1 1 1 的超时时间为 10 s 10s 10s,消息 3 3 3 的超时时间为 5 s 5s 5s,这可能会使得消息 3 3 3 的设置失效,因为消息队列也是队列,也必须遵守队列的先进先出的原则,所以队列的消息也会依次消费消息。所以延迟时间的设置一般建议是相等的,或者前小后大也可以。
当队列设置了超时时间,消息也设置了超时时间,一般以短的为准,谁短谁就生效。
实现延迟队列
延迟任务通过消息的TTL来实现。我们需要建立2个队列,一个用于发送消息,一个用于消息过期后的转发目标队列。
生产者输出消息到Queue1,并且这个消息是设置有有效时间的,比如60s。消息会在Queue1中等待60s,如果没有消费者收掉的话,它就是被转发到Queue2,Queue2有消费者,收到处理延迟任务。
没有消费者的队列,而且设置了超时时间,这个队列就称为**死信队列
**。
A服务把消息发送到queue1,但此时的queue1没有消费者 且设置了TTL,所以queue1为死信队列,过了TTL这个时间,就会把消息过期转发到queue2中,而queue2有消费者 B服务,可以即时消费,实现了延迟队列。
创建路由交换机
创建死信队列
创建死信转发队列
交换机队列绑定
测试
➢ 消息生产者
消息应该发送到死信队列。
channel.basicPublish("delay_exchange", "k1", null, msg.getBytes());
➢ 消息消费者
由另外一个队列接受消息。
channel.basicConsume("delay_queue2", true, consumer);
发送消息,通过控制台的日志可以看到是 10 s 10s 10s 后才收到消息:
添加MQ依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.amqpgroupId>
<artifactId>spring-rabbit-testartifactId>
<scope>testscope>
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);
}
}
消费者
@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);
}
}
}