RabbitMQ
RabbitMQ的安装部署
CentOS7安装RabbitMQ单机版(3.8.4)
Docker安装RabbitMQ集群
RabbitMQ默认端口:5672;UI端口:15672;集群端口:25672;
1. MQ的作用
- 异步
- 解耦
- 削峰
2. RabbitMQ的工作模型
RabbitMQ是用Erlang语言编写的
特性:
高可靠
灵活的路由
支持多客户端
集群与扩展性
高可用队列
权限管理
插件系统
与Spring集成 Spring AMQP
工作模型:
Broker: 主机;VHost: 虚拟主机;Exchange: 交换机;Binding: 绑定;Queue: 队列;Channel: 信道
利用VHost虚拟主机可以实现多租户的权限隔离,使用时互相之间不影响。
3. Exchange消息路由
- Direct 直连
- Topic 主题
*代表一个单词#代表0个或多个单词.隔开的代表一个单词,a.bc.def:abcdef三个单词
- Fanout 广播
4. 基本使用 Java API
安装RabbitMQ(需要先安装Erlang):略
Java使用RabbitMQ
4.1 引入依赖
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.6.0</version>
</dependency>
</dependencies>
4.2 消费者
package com.gupaoedu.simple;
import com.rabbitmq.client.*;
import java.io.IOException;
public class MyConsumer {
private final static String EXCHANGE_NAME = "SIMPLE_EXCHANGE";
private final static String QUEUE_NAME = "SIMPLE_QUEUE";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
// 连接IP
factory.setHost("127.0.0.1");
// 默认监听端口
factory.setPort(5672);
// 虚拟机 VHost
factory.setVirtualHost("/");
// 设置访问的用户 默认guest
factory.setUsername("guest");
factory.setPassword("guest");
// 建立连接
Connection conn = factory.newConnection();
// 创建消息通道
Channel channel = conn.createChannel();
// 声明交换机(创建交换机,已存在不会报错)
// String exchange 交换机名称
// String type 类型
// boolean durable 是否持久化
// boolean autoDelete 是否自动删除
// Map<String, Object> arguments 其他参数
channel.exchangeDeclare(EXCHANGE_NAME,"direct",false, false, null);
// 声明队列
// String queue 队列名称
// boolean durable 是否持久化
// boolean exclusive 是否排外的
// boolean autoDelete 是否自动删除
// Map<String, Object> arguments 其他参数
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" Waiting for message....");
// 绑定队列和交换机(绑定键gupao.best)
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "gupao.best");
// 创建消费者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("Received message : '" + msg + "'");
System.out.println("consumerTag : " + consumerTag );
System.out.println("deliveryTag : " + envelope.getDeliveryTag() );
}
};
// 开始获取消息
// String queue, boolean autoAck, Consumer callback
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
4.3 生产者
package com.gupaoedu.simple;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class MyProducer {
private final static String EXCHANGE_NAME = "SIMPLE_EXCHANGE";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
// 连接IP
factory.setHost("127.0.0.1");
// 连接端口
factory.setPort(5672);
// 虚拟机 VHost
factory.setVirtualHost("/");
// 用户 默认guest
factory.setUsername("guest");
factory.setPassword("guest");
// 建立连接
Connection conn = factory.newConnection();
// 创建消息通道
Channel channel = conn.createChannel();
String msg = "Hello world, Rabbit MQ";
// 发送消息
// String exchange, String routingKey, BasicProperties props, byte[] body
channel.basicPublish("QUEUE_NAME", "gupao.best", null, msg.getBytes());
channel.close();
conn.close();
}
}
4.4 参数详解(待补充)
5. 死信 Dead Letter 队列
- 死信交换机 DLX
- 死信队列 DLQ
5.1 TTL:time to live
x-message-ttl:队列可设置消息过期时间(对队列内所有消息生效)
.expiration("10000"):对每条消息设置过期时间(只对当前消息生效)
两种方式设置消息过期时间的方式都使用了,将以较小的数值为准
package com.gupaoedu.ttl;
import com.gupaoedu.util.ResourceUtil;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.util.HashMap;
import java.util.Map;
public class TTLProducer {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));
// 建立连接
Connection conn = factory.newConnection();
// 创建消息通道
Channel channel = conn.createChannel();
String msg = "Hello world, Rabbit MQ, DLX MSG";
// 通过队列属性设置消息过期时间
Map<String, Object> argss = new HashMap<String, Object>();
argss.put("x-message-ttl",6000);
// 声明队列(默认交换机AMQP default,Direct)
// String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
channel.queueDeclare("TEST_TTL_QUEUE", false, false, false, argss);
// 对每条消息设置过期时间
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.deliveryMode(2) // 持久化消息
.contentEncoding("UTF-8")
.expiration("10000") // TTL
.build();
// 此处两种方式设置消息过期时间的方式都使用了,将以较小的数值为准
// 发送消息 未指定交换机会发到默认的交换机上
channel.basicPublish("", "TEST_DLX_QUEUE", properties, msg.getBytes());
channel.close();
conn.close();
}
}
5.2 死信交换机和死信队列
创建队列的时候设置参数:"x-dead-letter-exchange" = "DLX_EXCHANGE"
如果设置了死信队列,触发以下条件会进入死信队列:
- 消息超时未被消费(TTL)
- 消息被消费者拒绝并且被设置不重回队列
- 消息超过队列最大数量或最大存储空间(x-max-length、x-max-length-bytes)
消息流转过程:
package com.gupaoedu.dlx;
import com.gupaoedu.util.ResourceUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 消息消费者,由于消费的代码被注释掉了,
* 10秒钟后,消息会从正常队列 TEST_DLX_QUEUE 到达死信交换机 DLX_EXCHANGE ,然后路由到死信队列DLX_QUEUE
*
*/
public class DlxConsumer {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));
// 建立连接
Connection conn = factory.newConnection();
// 创建消息通道
Channel channel = conn.createChannel();
// 指定队列的死信交换机
Map<String,Object> arguments = new HashMap<String,Object>();
arguments.put("x-dead-letter-exchange","DLX_EXCHANGE");
// arguments.put("x-expires","9000"); // 设置队列的TTL
// arguments.put("x-max-length", 4); // 如果设置了队列的最大长度,超过长度时,先入队的消息会被发送到DLX
// 声明队列(默认交换机AMQP default,Direct)
// String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
channel.queueDeclare("TEST_DLX_QUEUE", false, false, false, arguments);
// 声明死信交换机
channel.exchangeDeclare("DLX_EXCHANGE","topic", false, false, false, null);
// 声明死信队列
channel.queueDeclare("DLX_QUEUE", false, false, false, null);
// 绑定,此处 Dead letter routing key 设置为 #
channel.queueBind("DLX_QUEUE","DLX_EXCHANGE","#");
System.out.println(" Waiting for message....");
// 创建消费者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("Received message : '" + msg + "'");
}
};
// 开始获取消息
// String queue, boolean autoAck, Consumer callback
// channel.basicConsume("TEST_DLX_QUEUE", true, consumer);
}
}
package com.gupaoedu.dlx;
import com.gupaoedu.util.ResourceUtil;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.impl.AMQBasicProperties;
import java.util.HashMap;
import java.util.Map;
/**
* @Author: qingshan
* 消息生产者,通过TTL测试死信队列
*/
public class DlxProducer {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));
// 建立连接
Connection conn = factory.newConnection();
// 创建消息通道
Channel channel = conn.createChannel();
String msg = "Hello world, Rabbit MQ, DLX MSG";
// 设置属性,消息10秒钟过期
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.deliveryMode(2) // 持久化消息
.contentEncoding("UTF-8")
.expiration("10000") // TTL
.build();
// 发送消息 未指定交换机会发到默认的交换机上
channel.basicPublish("", "TEST_DLX_QUEUE", properties, msg.getBytes());
channel.close();
conn.close();
}
}
6. 延迟队列(插件)
需要用到rabbitmq_delayed_message_exchange插件来实现消息的延迟投递
6.1 安装插件
基于延时插件实现的延时队列(需连接到Linux系统的服务),需要安装延时队列插件: www.rabbitmq.com/community-p… github.com/rabbitmq/ra…
$ cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.12/plugins
$ wget https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/download/3.9.0/rabbitmq_delayed_message_exchange-3.9.0.ez
$ rabbitmq-plugins enable rabbitmq_delayed_message_exchange
6.2 使用
使用
x-delayed-message类型声明一个Exchange使用自定义标头
x-delay发布消息,以毫秒为单位表示消息的延迟时间。消息将在x-delay毫秒后传递到相应的队列
package com.gupaoedu.dlx;
import com.gupaoedu.util.ResourceUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* 使用延时插件实现的消息投递-消费者
* 必须要在服务端安装rabbitmq-delayed-message-exchange插件,安装步骤见README.MD
* 先启动消费者
*/
public class DelayPluginConsumer {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://guest:guest@127.0.0.1:5672");
// 建立连接
Connection conn = factory.newConnection();
// 创建消息通道
Channel channel = conn.createChannel();
// 声明x-delayed-message类型的exchange
Map<String, Object> argss = new HashMap<String, Object>();
argss.put("x-delayed-type", "direct");
channel.exchangeDeclare("DELAY_EXCHANGE", "x-delayed-message", false,
false, argss);
// 声明队列
channel.queueDeclare("DELAY_QUEUE", false,false,false,null);
// 绑定交换机与队列
channel.queueBind("DELAY_QUEUE", "DELAY_EXCHANGE", "DELAY_KEY");
// 创建消费者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
SimpleDateFormat sf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
System.out.println("收到消息:[" + msg + "]\n接收时间:" +sf.format(new Date()));
}
};
// 开始获取消息
// String queue, boolean autoAck, Consumer callback
channel.basicConsume("DELAY_QUEUE", true, consumer);
}
}
package com.gupaoedu.dlx;
import com.gupaoedu.util.ResourceUtil;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* 使用延时插件实现的消息投递-生产者
* 必须要在服务端安装rabbitmq-delayed-message-exchange插件,安装步骤见README.MD
* 先启动消费者
*/
public class DelayPluginProducer {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://guest:guest@127.0.0.1:5672");
// 建立连接
Connection conn = factory.newConnection();
// 创建消息通道
Channel channel = conn.createChannel();
// 延时投递,比如延时1分钟
Date now = new Date();
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, +1);// 1分钟后投递
Date delayTime = calendar.getTime();
// 定时投递,把这个值替换delayTime即可
// Date exactDealyTime = new Date("2019/01/14,22:30:00");
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
String msg = "发送时间:" + sf.format(now) + ",需要投递时间:" + sf.format(delayTime);
// 延迟的间隔时间,目标时刻减去当前时刻
Map<String, Object> headers = new HashMap<String, Object>();
headers.put("x-delay", delayTime.getTime() - now.getTime());
AMQP.BasicProperties.Builder props = new AMQP.BasicProperties.Builder()
.headers(headers);
channel.basicPublish("DELAY_EXCHANGE", "DELAY_KEY", props.build(),
msg.getBytes());
channel.close();
conn.close();
}
}
7. 流量控制
7.1 服务端流控(删除先入队或拒收)
- 设置队列容量(会删除先入队的消息)
- x-max-length 最大数量
- x-max-length-bytes 最大存储空间
- 内存控制(达到设定值不再接收消息)
- RabbitMQ配置文件中设置:vm_memory_high_watermark = 0.4 (40%)
- 磁盘控制(达到设定值不再接收消息)
- disk_free_limit.relative = 0.3 (30%)
- disk_free_limit.absolute = 2GB
7.2 消费端流控
prefetch count:非自动确认消息的前提下,如果一定数目的消息(通过基于consume或者channel设置Qos的值)未被确认前,不进行消费新的消息。消费者未回复的消息数量,即消费者正在处理的数量
代码示例:
package com.gupaoedu.limit;
import com.gupaoedu.util.ResourceUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* 消息消费者,测试消费端限流,先启动
*/
public class LimitConsumer {
private final static String QUEUE_NAME = "TEST_LIMIT_QUEUE";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));
// 建立连接
Connection conn = factory.newConnection();
// 创建消息通道
final Channel channel = conn.createChannel();
// 声明队列(默认交换机AMQP default,Direct)
// String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println("Consumer1 Waiting for message....");
// 创建消费者,并接收消息
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("Consumer1 Received message : '" + msg + "'" );
channel.basicAck(envelope.getDeliveryTag(), true); // 手动应答
}
};
// 非自动确认消息的前提下,如果一定数目的消息(通过基于consume或者channel设置Qos的值)未被确认前,不进行消费新的消息。
channel.basicQos(2); // prefetch count
channel.basicConsume(QUEUE_NAME, false, consumer); // false:非自动应答(手动应答)
}
}
多消费者
- 多个消费者监听同一个队列,RabbitMQ会进行消息的轮询分发(每条消息只会发送给一个消费者)
如果某些消费者任务繁重,来不及消费那么多消息,而某些其他消费者由于某些原因很快处理完了所分配到的消息,进而空闲,这样就会造成整体应用吞吐量的下降
- 可通过 channel.basicQos(x); 限制信道上消费者所能保持的最大未确认消息的数(通过消费端限流来平衡)
8. Spring AMQP
8.1 引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gupaoedu</groupId>
<artifactId>spring-rabbitmq</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<!-- spring版本号 -->
<spring.version>4.3.14.RELEASE</spring.version>
<!-- log4j日志文件管理包版本 -->
<slf4j.version>1.6.6</slf4j.version>
<log4j.version>1.2.12</log4j.version>
<!-- junit版本号 -->
<junit.version>4.10</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<!--rabbitmq依赖 -->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>1.3.5.RELEASE</version>
</dependency>
<!-- 日志文件管理包 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- log end -->
<!--单元测试依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
8.2 配置applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
<import resource="classpath*:rabbitMQ.xml" />
<!-- 扫描指定package下所有带有如 @Controller,@Service,@Resource 并把所注释的注册为Spring Beans -->
<context:component-scan base-package="com.gupaoedu.*" />
<!-- 激活annotation功能 -->
<context:annotation-config />
<!-- 激活annotation功能 -->
<context:spring-configured />
</beans>
log4j.properties
log4j.rootLogger=INFO,consoleAppender,fileAppender
log4j.category.ETTAppLogger=DEBUG, ettAppLogFile
log4j.appender.consoleAppender=org.apache.log4j.ConsoleAppender
log4j.appender.consoleAppender.Threshold=TRACE
log4j.appender.consoleAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.consoleAppender.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss SSS} ->[%t]--[%-5p]--[%c{1}]--%m%n
log4j.appender.fileAppender=org.apache.log4j.DailyRollingFileAppender
log4j.appender.fileAppender.File=F:/dev_logs/rabbitmq/debug1.log
log4j.appender.fileAppender.DatePattern='_'yyyy-MM-dd'.log'
log4j.appender.fileAppender.Threshold=TRACE
log4j.appender.fileAppender.Encoding=BIG5
log4j.appender.fileAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.fileAppender.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss SSS}-->[%t]--[%-5p]--[%c{1}]--%m%n
log4j.appender.ettAppLogFile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.ettAppLogFile.File=F:/dev_logs/rabbitmq/ettdebug.log
log4j.appender.ettAppLogFile.DatePattern='_'yyyy-MM-dd'.log'
log4j.appender.ettAppLogFile.Threshold=DEBUG
log4j.appender.ettAppLogFile.layout=org.apache.log4j.PatternLayout
log4j.appender.ettAppLogFile.layout.ConversionPattern=%-d{yyyy-MM-dd HH\:mm\:ss SSS}-->[%t]--[%-5p]--[%c{1}]--%m%n
8.3 配置rabbitMQ.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit-1.2.xsd">
<!--配置connection-factory,指定连接rabbit server参数 -->
<rabbit:connection-factory id="connectionFactory" virtual-host="/" username="guest" password="guest" host="127.0.0.1" port="5672" />
<!--通过指定下面的admin信息,当前producer中的exchange和queue会在rabbitmq服务器上自动生成 -->
<rabbit:admin id="connectAdmin" connection-factory="connectionFactory" />
<!--############分隔线############-->
<!--定义queue -->
<rabbit:queue name="MY_FIRST_QUEUE" durable="true" auto-delete="false" exclusive="false" declared-by="connectAdmin" />
<!--定义direct exchange,绑定MY_FIRST_QUEUE -->
<rabbit:direct-exchange name="MY_DIRECT_EXCHANGE" durable="true" auto-delete="false" declared-by="connectAdmin">
<rabbit:bindings>
<rabbit:binding queue="MY_FIRST_QUEUE" key="FirstKey">
</rabbit:binding>
</rabbit:bindings>
</rabbit:direct-exchange>
<!--定义rabbit template用于数据的接收和发送 -->
<rabbit:template id="amqpTemplate" connection-factory="connectionFactory" exchange="MY_DIRECT_EXCHANGE" />
<!--消息接收者 -->
<bean id="messageReceiver" class="com.gupaoedu.consumer.FirstConsumer"></bean>
<!--queue listener 观察 监听模式 当有消息到达时会通知监听在对应的队列上的监听对象 -->
<rabbit:listener-container connection-factory="connectionFactory">
<rabbit:listener queues="MY_FIRST_QUEUE" ref="messageReceiver" />
</rabbit:listener-container>
<!--定义queue -->
<rabbit:queue name="MY_SECOND_QUEUE" durable="true" auto-delete="false" exclusive="false" declared-by="connectAdmin" />
<!-- 将已经定义的Exchange绑定到MY_SECOND_QUEUE,注意关键词是key -->
<rabbit:direct-exchange name="MY_DIRECT_EXCHANGE" durable="true" auto-delete="false" declared-by="connectAdmin">
<rabbit:bindings>
<rabbit:binding queue="MY_SECOND_QUEUE" key="SecondKey"></rabbit:binding>
</rabbit:bindings>
</rabbit:direct-exchange>
<!-- 消息接收者 -->
<bean id="receiverSecond" class="com.gupaoedu.consumer.SecondConsumer"></bean>
<!-- queue litener 观察 监听模式 当有消息到达时会通知监听在对应的队列上的监听对象 -->
<rabbit:listener-container connection-factory="connectionFactory">
<rabbit:listener queues="MY_SECOND_QUEUE" ref="receiverSecond" />
</rabbit:listener-container>
<!--############分隔线############-->
<!--定义queue -->
<rabbit:queue name="MY_THIRD_QUEUE" durable="true" auto-delete="false" exclusive="false" declared-by="connectAdmin" />
<!-- 定义topic exchange,绑定MY_THIRD_QUEUE,注意关键词是pattern -->
<rabbit:topic-exchange name="MY_TOPIC_EXCHANGE" durable="true" auto-delete="false" declared-by="connectAdmin">
<rabbit:bindings>
<rabbit:binding queue="MY_THIRD_QUEUE" pattern="#.Third.#"></rabbit:binding>
</rabbit:bindings>
</rabbit:topic-exchange>
<!--定义rabbit template用于数据的接收和发送 -->
<rabbit:template id="amqpTemplate2" connection-factory="connectionFactory" exchange="MY_TOPIC_EXCHANGE" />
<!-- 消息接收者 -->
<bean id="receiverThird" class="com.gupaoedu.consumer.ThirdConsumer"></bean>
<!-- queue litener 观察 监听模式 当有消息到达时会通知监听在对应的队列上的监听对象 -->
<rabbit:listener-container connection-factory="connectionFactory">
<rabbit:listener queues="MY_THIRD_QUEUE" ref="receiverThird" />
</rabbit:listener-container>
<!--############分隔线############-->
<!--定义queue -->
<rabbit:queue name="MY_FOURTH_QUEUE" durable="true" auto-delete="false" exclusive="false" declared-by="connectAdmin" />
<!-- 定义fanout exchange,绑定MY_FIRST_QUEUE 和 MY_FOURTH_QUEUE -->
<rabbit:fanout-exchange name="MY_FANOUT_EXCHANGE" auto-delete="false" durable="true" declared-by="connectAdmin" >
<rabbit:bindings>
<rabbit:binding queue="MY_FIRST_QUEUE"></rabbit:binding>
<rabbit:binding queue="MY_FOURTH_QUEUE"></rabbit:binding>
</rabbit:bindings>
</rabbit:fanout-exchange>
<!-- 消息接收者 -->
<bean id="receiverFourth" class="com.gupaoedu.consumer.FourthConsumer"></bean>
<!-- queue litener 观察 监听模式 当有消息到达时会通知监听在对应的队列上的监听对象 -->
<rabbit:listener-container connection-factory="connectionFactory">
<rabbit:listener queues="MY_FOURTH_QUEUE" ref="receiverFourth" />
</rabbit:listener-container>
</beans>
<rabbit:connection-factory
<rabbit:admin
<rabbit:queue
<rabbit:direct-exchange
<rabbit:bindings
<rabbit:template
<rabbit:listener-container
效果图示:
8.4 消费者
FirstConsumer
package com.gupaoedu.consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
public class FirstConsumer implements MessageListener {
private Logger logger = LoggerFactory.getLogger(FirstConsumer.class);
public void onMessage(Message message) {
logger.info("The first consumer received message : " + message.getBody());
}
}
SecondConsumer
public class SecondConsumer implements MessageListener {
private Logger logger = LoggerFactory.getLogger(SecondConsumer.class);
public void onMessage(Message message) {
logger.info("The second consumer received message : " + message);
}
}
ThirdConsumer
public class ThirdConsumer implements MessageListener {
private Logger logger = LoggerFactory.getLogger(ThirdConsumer.class);
public void onMessage(Message message) {
logger.info("The third cosumer received message : " + message);
}
}
FourthConsumer
public class FourthConsumer implements MessageListener {
private Logger logger = LoggerFactory.getLogger(FourthConsumer.class);
public void onMessage(Message message) {
logger.info("The fourth consumer received message : " + message);
}
}
8.5 生产者
package com.gupaoedu.producer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
/**
* 消息生产者
*/
@Service
public class MessageProducer {
private Logger logger = LoggerFactory.getLogger(MessageProducer.class);
@Autowired
@Qualifier("amqpTemplate")
private AmqpTemplate amqpTemplate;
@Autowired
@Qualifier("amqpTemplate2")
private AmqpTemplate amqpTemplate2;
/**
* 演示三种交换机的使用
*
* @param message
*/
public void sendMessage(Object message) {
logger.info("Send message:" + message);
// amqpTemplate 默认交换机 MY_DIRECT_EXCHANGE
// amqpTemplate2 默认交换机 MY_TOPIC_EXCHANGE
// Exchange 为 direct 模式,直接指定routingKey
amqpTemplate.convertAndSend("FirstKey", "[Direct,FirstKey] "+message);
amqpTemplate.convertAndSend("SecondKey", "[Direct,SecondKey] "+message);
// Exchange模式为topic,通过topic匹配关心该主题的队列
amqpTemplate2.convertAndSend("msg.Third.send","[Topic,msg.Third.send] "+message);
// 广播消息,与Exchange绑定的所有队列都会收到消息,routingKey为空
amqpTemplate2.convertAndSend("MY_FANOUT_EXCHANGE",null,"[Fanout] "+message);
}
}
8.6 测试类
package com.gupaoedu.test;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.gupaoedu.producer.MessageProducer;
public class RabbitTest {
private ApplicationContext context = null;
@Test
public void sendMessage() {
context = new ClassPathXmlApplicationContext("applicationContext.xml");
MessageProducer messageProducer = (MessageProducer) context.getBean("messageProducer");
int k = 100;
while (k > 0) {
messageProducer.sendMessage("第" + k + "次发送的消息");
k--;
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
9. Spring Boot 集成 RabbitMQ
- 对象定义:创建交换机、队列、绑定
- 监听关系:消费者类监听队列
- 发送消息:注入模板发送消息
案例:
9.1 消费者工程
- 依赖和配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gupaoedu</groupId>
<artifactId>springboot-rabbit-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>springboot-rabbit-consumer</name>
<description>Demo project for RabbitMQ Consumer</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.5</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.21</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
server.port=9072
// 手动ack
spring.rabbitmq.listener.direct.acknowledge-mode=manual
spring.rabbitmq.listener.simple.acknowledge-mode=manual
spring.rabbitmq.cache.channel.size=
gupaomq.properties
com.gupaoedu.directexchange=GP_DIRECT_EXCHANGE
com.gupaoedu.topicexchange=GP_TOPIC_EXCHANGE
com.gupaoedu.fanoutexchange=GP_FANOUT_EXCHANGE
com.gupaoedu.firstqueue=GP_FIRST_QUEUE
com.gupaoedu.secondqueue=GP_SECOND_QUEUE
com.gupaoedu.thirdqueue=GP_THIRD_QUEUE
com.gupaoedu.fourthqueue=GP_FOURTH_QUEUE
package com.gupaoedu.entity;
import java.io.Serializable;
public class Merchant implements Serializable {
int id; // 商户编号
String name; // 商户名称
String address; // 商户地址
String accountNo; // 商户账号
String accountName; // 户名
String state; // 状态 1 激活 2 关闭
String stateStr; // 状态中文
public Merchant() {
}
public Merchant(int id, String name, String address) {
this.id = id;
this.name = name;
this.address = address;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public String getAccountName() {
return accountName;
}
public void setAccountName(String accountName) {
this.accountName = accountName;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getStateStr() {
if(null == state){
return "";
}else if("1".equals(state.toString())){
return "激活";
}else if ("0".equals(state.toString())){
return "关闭";
}else{
return "未知";
}
}
public void setStateStr(String stateStr) {
this.stateStr = state;
}
@Override
public String toString() {
return "Merchant{" +
"id=" + id +
", name='" + name + '\'' +
", address='" + address + '\'' +
", accountNo='" + accountNo + '\'' +
", accountName='" + accountName + '\'' +
", state='" + state + '\'' +
", stateStr='" + stateStr + '\'' +
'}';
}
}
- RabbitMQ配置类 RabbitConfig
package com.gupaoedu.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.RabbitListenerConfigurer;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistrar;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory;
import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory;
@Configuration
@PropertySource("classpath:gupaomq.properties")
public class RabbitConfig {
@Value("${com.gupaoedu.firstqueue}")
private String firstQueue;
@Value("${com.gupaoedu.secondqueue}")
private String secondQueue;
@Value("${com.gupaoedu.thirdqueue}")
private String thirdQueue;
@Value("${com.gupaoedu.fourthqueue}")
private String fourthQueue;
@Value("${com.gupaoedu.directexchange}")
private String directExchange;
@Value("${com.gupaoedu.topicexchange}")
private String topicExchange;
@Value("${com.gupaoedu.fanoutexchange}")
private String fanoutExchange;
// 创建四个队列
@Bean("vipFirstQueue")
public Queue getFirstQueue(){
return new Queue(firstQueue);
}
@Bean("vipSecondQueue")
public Queue getSecondQueue(){
return new Queue(secondQueue);
}
@Bean("vipThirdQueue")
public Queue getThirdQueue(){
return new Queue(thirdQueue);
}
@Bean("vipFourthQueue")
public Queue getFourthQueue(){
return new Queue(fourthQueue);
}
// 创建三个交换机
@Bean("vipDirectExchange")
public DirectExchange getDirectExchange(){
return new DirectExchange(directExchange);
}
@Bean("vipTopicExchange")
public TopicExchange getTopicExchange(){
return new TopicExchange(topicExchange);
}
@Bean("vipFanoutExchange")
public FanoutExchange getFanoutExchange(){
return new FanoutExchange(fanoutExchange);
}
// 定义四个绑定关系
@Bean
public Binding bindFirst(@Qualifier("vipFirstQueue") Queue queue, @Qualifier("vipDirectExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("gupao.best");
}
@Bean
public Binding bindSecond(@Qualifier("vipSecondQueue") Queue queue, @Qualifier("vipTopicExchange") TopicExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("*.gupao.*");
}
@Bean
public Binding bindThird(@Qualifier("vipThirdQueue") Queue queue, @Qualifier("vipFanoutExchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
@Bean
public Binding bindFourth(@Qualifier("vipFourthQueue") Queue queue, @Qualifier("vipFanoutExchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
/**
* 在消费端转换JSON消息
* 监听类都要加上containerFactory属性
* @param connectionFactory
* @return
*/
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
factory.setAutoStartup(true);
return factory;
}
}
- 消费者
@Component
@PropertySource("classpath:gupaomq.properties")
@RabbitListener(queues = "${com.gupaoedu.firstqueue}", containerFactory="rabbitListenerContainerFactory")
public class FirstConsumer {
@RabbitHandler
public void process(@Payload Merchant merchant){
System.out.println("First Queue received msg : " + merchant.getName());
}
}
@Component
@PropertySource("classpath:gupaomq.properties")
@RabbitListener(queues = "${com.gupaoedu.secondqueue}", containerFactory="rabbitListenerContainerFactory")
public class SecondConsumer {
@RabbitHandler
public void process(String msgContent,Channel channel, Message message) throws IOException {
System.out.println("Second Queue received msg : " + msgContent );
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
@Component
@PropertySource("classpath:gupaomq.properties")
@RabbitListener(queues = "${com.gupaoedu.thirdqueue}", containerFactory="rabbitListenerContainerFactory")
public class ThirdConsumer {
@RabbitHandler
public void process(String msg){
System.out.println("Third Queue received msg : " + msg);
}
}
@Component
@PropertySource("classpath:gupaomq.properties")
@RabbitListener(queues = "${com.gupaoedu.fourthqueue}", containerFactory="rabbitListenerContainerFactory")
public class FourthConsumer {
@RabbitHandler
public void process(String message) throws IOException {
System.out.println("Fourth Queue received msg : " + message);
}
}
- 启动类
@SpringBootApplication
public class RabbitConsumerApp {
public static void main(String[] args) {
SpringApplication.run(RabbitConsumerApp.class, args);
}
}
9.2 生产者工程
实战demo见工程springboot-project:springboot-rabbit-producer
Spring AMQP默认使用的消息转换器是SimpleMessageConverter。
Jackson2JsonMessageConverter 用于将消息转换为JSON后发送。
com.gupaoedu.directexchange=GP_DIRECT_EXCHANGE
com.gupaoedu.topicexchange=GP_TOPIC_EXCHANGE
com.gupaoedu.fanoutexchange=GP_FANOUT_EXCHANGE
com.gupaoedu.directroutingkey=gupao.best
com.gupaoedu.topicroutingkey1=shanghai.gupao.teacher
com.gupaoedu.topicroutingkey2=changsha.gupao.student
@Configuration
public class RabbitConfig {
/**
* 所有的消息发送都会转换成JSON格式发到交换机
* @param connectionFactory
* @return
*/
@Bean
public RabbitTemplate gupaoTemplate(final ConnectionFactory connectionFactory) {
final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
return rabbitTemplate;
}
}
package com.gupaoedu.producer;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gupaoedu.entity.Merchant;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
@Component
@PropertySource("classpath:gupaomq.properties")
public class RabbitSender {
@Value("${com.gupaoedu.directexchange}")
private String directExchange;
@Value("${com.gupaoedu.topicexchange}")
private String topicExchange;
@Value("${com.gupaoedu.fanoutexchange}")
private String fanoutExchange;
@Value("${com.gupaoedu.directroutingkey}")
private String directRoutingKey;
@Value("${com.gupaoedu.topicroutingkey1}")
private String topicRoutingKey1;
@Value("${com.gupaoedu.topicroutingkey2}")
private String topicRoutingKey2;
// 自定义的模板gupaoTemplate,所有的消息都会转换成JSON发送
@Autowired
AmqpTemplate gupaoTemplate;
public void send() throws JsonProcessingException {
Merchant merchant = new Merchant(1001,"a direct msg : 中原镖局","汉中省解放路266号");
gupaoTemplate.convertAndSend(directExchange,directRoutingKey, merchant);
gupaoTemplate.convertAndSend(topicExchange,topicRoutingKey1, "a topic msg : shanghai.gupao.teacher");
gupaoTemplate.convertAndSend(topicExchange,topicRoutingKey2, "a topic msg : changsha.gupao.student");
// 发送JSON字符串
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(merchant);
System.out.println(json);
gupaoTemplate.convertAndSend(fanoutExchange,"", json);
}
}
测试类
package com.gupaoedu;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.gupaoedu.producer.RabbitSender;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* 记得一定要先启动消费者,否则交换机和队列以及绑定关系都不会创建
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class AppTests {
@Autowired
RabbitSender rabbitSender;
@Test
public void contextLoads() throws JsonProcessingException {
// 先启动消费者 consumer,否则交换机、队列、绑定都不存在
rabbitSender.send();
}
}
10. 消息可靠性投递
- 生产者发送消息给Broker 的可靠性
- Exchange路由给Queue 的可靠性
- 队列存储消息 的可靠性
- 消费者消费消息 的可靠性(Queue到Consumer)
10.1 服务端确认(事务消息)
1. Transaction 事务模式
// 将channel设置成事务模式
channel.txSelect();
// 发送消息。String exchange, String routingKey, BasicProperties props, byte[] body
channel.basicPublish("", QUEUE_NAME, null, (msg).getBytes());
// ...
// 无问题 提交事务
channel.txCommit();
// try/catch 有异常 事务回滚
channel.txRollback();
2. Confirm 确认模式
普通模式
// 开启发送方确认模式:将channel设置为Confirm模式
channel.confirmSelect();
// 发送消息
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
// ①普通Confirm,发送一条,确认一条
if (channel.waitForConfirms()) {
// 消息发送成功
}
批量确认模式
try {
channel.confirmSelect();
for (int i = 0; i < 1000; i++) {
// 发送消息
// String exchange, String routingKey, BasicProperties props, byte[] body
channel.basicPublish("", QUEUE_NAME, null, (msg +"-"+ i).getBytes());
}
// 批量确认结果,ACK如果是Multiple=True,代表ACK里面的Delivery-Tag之前的消息都被确认了
// 比如5条消息可能只收到1个ACK,也可能收到2个(抓包才看得到)
// 直到所有信息都发布,只要有一个未被Broker确认就会IOException
channel.waitForConfirmsOrDie();
System.out.println("消息发送完毕,批量确认成功");
} catch (Exception e) {
// 发生异常,可能需要对所有消息进行重发
e.printStackTrace();
}
异步确认模式
// 用来维护未确认消息的deliveryTag
final SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());
// 这里不会打印所有响应的ACK;ACK可能有多个,有可能一次确认多条,也有可能一次确认一条
// 异步监听确认和未确认的消息
// 如果要重复运行,先停掉之前的生产者,清空队列
channel.addConfirmListener(new ConfirmListener() {
// 未确认的消息
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
// 如果true表示批量执行了deliveryTag这个值以前(小于deliveryTag的)的所有消息,如果为false的话表示单条确认
System.out.println("Broker未确认消息,标识:" + deliveryTag);
if (multiple) {
// headSet表示后面参数之前的所有元素,全部删除
confirmSet.headSet(deliveryTag + 1L).clear();
} else {
// 只移除一个元素
confirmSet.remove(deliveryTag);
}
// TODO 这里添加重发的方法
}
// 已经确认的消息
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
// 如果true表示批量执行了deliveryTag这个值以前(小于deliveryTag的)的所有消息,如果为false的话表示单条确认
System.out.println(String.format("Broker已确认消息,标识:%d,多个消息:%b", deliveryTag, multiple));
if (multiple) {
// headSet表示后面参数之前的所有元素,全部删除
confirmSet.headSet(deliveryTag + 1L).clear();
} else {
// 只移除一个元素
confirmSet.remove(deliveryTag);
}
System.out.println("未确认的消息:"+confirmSet);
}
});
// 开启发送方确认模式
channel.confirmSelect();
for (int i = 0; i < 10; i++) {
long nextSeqNo = channel.getNextPublishSeqNo();
// 发送消息
// String exchange, String routingKey, BasicProperties props, byte[] body
channel.basicPublish("", QUEUE_NAME, null, (msg +"-"+ i).getBytes());
confirmSet.add(nextSeqNo);
}
System.out.println("所有消息:"+confirmSet);
10.2 路由保证
默认情况下无法被路由的消息将会被丢弃。有两种方式来挽救:
-
配置
mandatory=true+ 回发监听器ReturnListener- 消息无法路由时将回发至ReturnListener
-
指定交换机的备份交换机
- 创建交换机的时候设置alternate-exchange参数
import com.gupaoedu.util.ResourceUtil;
/**
* 当消息无法匹配到队列时,会发回给生产者
*/
public class ReturnListenerProducer {
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
// IP、端口、用户名、密码、虚拟主机
// 默认使用 / 的vhost,如果修改vhost,加在端口后即可,如 /gphost
factory.setUri("amqp://guest:guest@127.0.0.1:5672");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.addReturnListener(new ReturnListener() {
public void handleReturn(int replyCode,
String replyText,
String exchange,
String routingKey,
AMQP.BasicProperties properties,
byte[] body)
throws IOException {
System.out.println("=========监听器收到了无法路由,被返回的消息=========");
System.out.println("replyText:"+replyText);
System.out.println("exchange:"+exchange);
System.out.println("routingKey:"+routingKey);
System.out.println("message:"+new String(body));
}
});
// 2 消息持久化
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder().deliveryMode(2).contentEncoding("UTF-8").build();
// 声明备份交换机
channel.exchangeDeclare("ALTERNATE_EXCHANGE","topic", false, false, false, null);
channel.queueDeclare("ALTERNATE_QUEUE", false, false, false, null);
channel.queueBind("ALTERNATE_QUEUE","ALTERNATE_EXCHANGE","#");
// 在声明交换机的时候指定备份交换机
Map<String,Object> arguments = new HashMap<String,Object>();
arguments.put("alternate-exchange","ALTERNATE_EXCHANGE");
channel.exchangeDeclare("TEST_EXCHANGE","topic", false, false, false, arguments);
// 发送到了默认的交换机上,由于没有任何队列使用这个关键字跟交换机绑定,所以会被退回
// 第三个参数是设置的mandatory,如果mandatory是false,消息也会被直接丢弃
channel.basicPublish("","gupaodirect",true, properties,"只为更好的你".getBytes());
TimeUnit.SECONDS.sleep(10);
channel.close();
connection.close();
}
}
10.3 消息存储
-
队列持久化 durable = true
-
交换机持久化 durable = true
-
消息持久化
- 上面的例子中2代码持久化,其他数字代表非持久化。AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder().deliveryMode(2).contentEncoding("UTF-8").build();
-
集群(防止单点故障)
10.4 消费者确认
- 默认的是自动应答(NONE):一接收到消息就会ack
- spring.rabbitmq.listener.direct.acknowledge-mode=manual spring.rabbitmq.listener.simple.acknowledge-mode=manual
- NONE自动ack、MANUAL手动ack、AUTO不发生异常的情况下自动ack
- channel.basicConsume(QUEUE_NAME, false, consumer); // false 关闭自动应答
- channel.basicAck(); // 手工应答
- channel.basicReject(); // 单条拒绝
- channel.basicNack(); // 批量拒绝
应答最好放到finally中,确保被执行,否则队列中的消息会一直存在
package com.gupaoedu.ack;
import com.gupaoedu.util.ResourceUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* 消息消费者,用于测试消费者手工应答和重回队列
*/
public class AckConsumer {
private final static String QUEUE_NAME = "TEST_ACK_QUEUE";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));
// 建立连接
Connection conn = factory.newConnection();
// 创建消息通道
final Channel channel = conn.createChannel();
// 声明队列(默认交换机AMQP default,Direct)
// String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" Waiting for message....");
// 创建消费者,并接收消息
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("Received message : '" + msg + "'");
if (msg.contains("拒收")){
// 拒绝消息
// 第二个参数 requeue:是否重回队列,true:是;false:直接丢弃,相当于告诉队列可以直接删除掉
// TODO 如果只有这一个消费者,requeue为true的时候会造成消息重复消费【死循环】
channel.basicReject(envelope.getDeliveryTag(), false);
} else if (msg.contains("异常")){
// 批量拒绝
// 第二个参数 multiple:是否批量.true:将一次性拒绝所有小于deliveryTag的消息
// 第三个参数 requeue:是否重回队列
// TODO 如果只有这一个消费者,requeue为true的时候会造成消息重复消费【死循环】
channel.basicNack(envelope.getDeliveryTag(), true, false);
} else {
// 手工应答
// 如果不应答,队列中的消息会一直存在,重新连接的时候会重复消费
// ... 业务逻辑的处理 ...
channel.basicAck(envelope.getDeliveryTag(), true);
}
}
};
// 开始获取消息,注意这里开启了手工应答
// String queue, boolean autoAck, Consumer callback
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
10.5 其他
1. 消费者回调
- 消费者调用生产者提供的API:通知生产者已处理完数据
- 给生产者发一条消息(回执)
2. 补偿机制(重发)
- 重发消息(消息重试机制)
- 时间间隔
- 重发次数
- 消息落库,保证重发消息一致
3. 最终一致性
- 对账(定时(比如每晚)进行批处理对账)
4. 消息的幂等性
- 全局唯一的业务ID/流水号
5. 消息的顺序性
- 一个消费者消费一个队列时才能保证顺序性
-
11. 集群和高可用
集群的目的:
- 高可用
- 负载均衡
RabbitMQ的节点类型
- disc(默认):磁盘节点。数据只存储到磁盘上面,是稳定、持久的
- 至少需要1个disc,所以单节点一定是disc
- 用来做稳定存储的节点
- RAM:内存节点。数据同时存储在内存和磁盘上
- 读写在内存中操作,持久化在磁盘中操作。读写效率比较高
- 可以做对外提供服务的节点
11.1 普通集群
节点间只同步元数据,不同步队列内容
同一个集群要保证cookie一致:/var/lib/rabbitmq/.erlang.cookie
11.2 镜像队列
1、节点之间同步所有数据,多个消费者连接到不同节点会消费到一样的数据
2、镜像队列的设计只保证了HA,并不能显著提升性能,反而因为增加节点消耗了更多硬件资源
11.3 高可用HA
HAProxy+Keepalived搭建RabbitMQ高可用集群:gper.club/articles/7e…
VRRP:虚拟路由器冗余协议
11.4 Docker安装RabbitMQ集群
12. 实践经验总结
-
资源管理
- 交换机、队列资源谁使用谁创建(消费者)
- 创建方式可以在代码中,或者在管理界面中统一创建(就像数据库的表一样)
-
配置文件和命名规范
- xxx_mq.properties:统一存放交换机名字、队列名字等
- 虚拟机后缀:_VHOST
- 交换机后缀:_EXCHANGE
- 队列后缀:_QUEUE
- 体现数据来源和去向:SALE_TO_PRODUCT_EXCHANGE(销售系统跟产品系统通信用的)
-
调用封装
-
自定义封装,简化参数
-
自定义封装,方便切换成其他的MQ产品
-
GpSendMsg(Object msg){ JmsTemplate.send(destination,msg); } GpSendMsg(Object msg){ RabbitTemplate.send(exchange,routingKey,msg); }
-
-
消息追溯与重发
- 如果要实现消息的可追溯:入库-消息表
- 如果要实现消息的重发:入库-消息表,加上发送状态字段
-
生产环境运维监控
- zabbix
- Grafana
-
日志追踪,消息的流入流出(影响性能,不建议做)
- Firehose
- Tracing GUI
-
连接数过多(如何减少连接数)
- 设计的时候兼容单条和多条数据的通信,多条消息合并发送减少连接次数