一文了解RabbitMQ其他模式(持久化、消息确认、消费端限流、死信队列)

438 阅读8分钟

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

1 持久化

  • 消息的可靠性是RabbitMQ的一大特色,那么RabbitMQ是如何避免消息丢失?

    • 消费者的ACK确认机制,可以防止消费者丢失消息

    • 万一在消费者消费之前,RabbitMQ服务器宕机了,那消息也会丢失

  • 想要将消息持久化,那么路由和队列都要持久化才可以

1.1 生产者

public	class	Sender	{
public	static	void	main(String[]	args)	throws	Exception	{
Connection connection=ConnectionUtil.getConnection();
Channel	channel=connection.createChannel();

//声明路由(路由名,路由类型,持久化)
channel.exchangeDeclare("test_exchange_topic","topic",true);
String	msg="商品降价";
//发送消息(第三个参数作用是让消息持久化)
channel.basicPublish("test_exchange_topic","product.price",
MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes());
System.out.println("[用户系统]:"+msg);
channel.close();
connection.close();
}
}

1.2 消费者

public	class	Recer1	{
public	static	void	main(String[]	args)	throws	Exception	{
Connection connection=ConnectionUtil.getConnection();
Channel	channel	=connection.createChannel();
//声明队列(第二个参数为true:支持持久化)
channel.queueDeclare("test_exchange_topic_queue_1",true,false,false,null);
channel.queueBind("test_exchange_topic_queue_1","test_exchange_topic",
"user.#");
DefaultConsumer	consumer =new	DefaultConsumer(channel){
@Override
public	void	handleDelivery(String	consumerTag,	Envelope	envelope,
AMQP.BasicProperties properties, byte[]	body)	throws	IOException	{
String	s =new	String(body);
System.out.println("【消费者1】="+s);
    }
};
channel.basicConsume("test_exchange_topic_queue_1",true,consumer);
    }
}

2 Spring整合RabbitMQ

  • 五种消息模型,在企业中应用最广泛的就是最后一种:定向匹配topic
  • Spring AMQP 是基于Spring 框架的AMQP消息解决方案,提供模板化的发送和接收消息的抽象层,提供基于消息驱动的POJO的消息监听等,简化了我们对于RabbitMQ相关程序的开发。

2.1 生产端工程

<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
  • spring-rabbitmq-producer.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.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">

<!--	1.配置连接	-->
<rabbit:connection-factory
id="connectionFactory"
host="192.168.204.141"
port="5672"
username="root"
password="123123"
virtual-host="/root"
/>
<!--	2.配置队列	-->
<rabbit:queue	name="test_spring_queue_1"/>
<!--	3.配置rabbitAdmin:主要用于在Java代码中对理队和队列进行管理,用于创建、绑定、删
除队列与交换机,发送消息等	-->
<rabbit:admin	connection-factory="connectionFactory"/>
<!--	4.配置topic类型exchange;队列绑定到交换机	-->
<rabbit:topic-exchange	name="spring_topic_exchange">
<rabbit:bindings>
<rabbit:binding	queue="test_spring_queue_1"	pattern="msg.#"/>
</rabbit:bindings>
</rabbit:topic-exchange>
<!--5.配置消息对象json转换类	-->
<beanid="jsonMessageConverter"
class="org.springframework.amqp.support.converter.Jackson2JsonMessageConverter"/>
<!--6.配置RabbitTemplate(消息生产者)	-->
<rabbit:templateid="rabbitTemplate"
connection-factory="connectionFactory"
exchange="spring_topic_exchange"
message-converter="jsonMessageConverter"/>

</beans>
  • 发消息
public class Sender {
public static void main(String[] args) {
// 1.创建spring容器
ClassPathXmlApplicationContext context = new
ClassPathXmlApplicationContext("spring/spring-rabbitmq-producer.xml");
// 2.从容器中获取对象
RabbitTemplate template = context.getBean(RabbitTemplate.class);
// 3.发送消息
Map<String, String> map = new HashMap();
map.put("name", "大佬孙");
map.put("email", "19998539@qq.com");
template.convertAndSend("msg.user", map);
context.close();
}
}

2.2 消费端工程

依赖与生产者一致

spring-rabbitmq-consumer.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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<!--1.配置连接	-->
<rabbit:connection-factory
id="connectionFactory"
host="192.168.204.141"
port="5672"
username="root"
password="123123"
virtual-host="/root"/>

<!--	2.	配置队列	-->
<rabbit:queue	name="test_spring_queue_1"/>
<!--	3.配置rabbitAdmin	-->
<rabbit:admin	connection-factory="connectionFactory"/>
<!--	4.springIOC注解扫描包-->
<context:component-scan	base-package="listener"/>
<!--	5.配置监听	-->
<rabbit:listener-container	connection-factory="connectionFactory">
<rabbit:listener	ref="consumerListener"	queue-
names="test_spring_queue_1"	/>
</rabbit:listener-container>
</beans>
  • 消费者

    • MessageListener接口用于spring容器接收到消息后处理消息
    • 如果需要使用自己定义的类型来实现处理消息时,必须实现该接口,并重写onMessage(方法
    • 当spring容器接收消息后,会自动交由onMessage进行处理
@Component
public	class	ConsumerListener implements MessageListener	{

//jackson提供序列化和反序列中使用最多的类,用来转换json的
private	static	final	ObjectMapper MAPPER= new	ObjectMapper();
public	void onMessage(Message message)	{
try	{
// 将message对象转换成json
JsonNode	jsonNode=MAPPER.readTree(message.getBody());
String	name=jsonNode.get("name").asText();
String	email=jsonNode.get("email").asText();
System.out.println("从队列中获取:【"+name+"的邮箱是:"+email+"】");
}catch	(Exception e){
e.printStackTrace();
}
}
}
  • 启动项目
public	class	TestRunner	{
public	static	void	main(String[]	args)	throws	Exception	{
//	获得容器
ClassPathXmlApplicationContext	context	=new ClassPathXmlApplicationContext("spring/spring-rabbitmq-consumer.xml");
//让程序一直运行,别终止
System.in.read();
}
}

3 消息成功确认机制

在实际场景下,有的生产者发送的消息是必须保证成功发送到消息队列中,那么如何保证成功投递呢?

  • 事务机制
  • 发布确认机制

3.1 事务机制

  • AMQP协议提供的一种保证消息成功投递的方式,通过信道开启 transactional 模式

  • 并利用信道 的三个方法来实现以事务方式 发送消息,若发送失败,通过异常处理回滚事务,确保消息成功投递

    • channel.txSelect():开启事务
    • channel.txCom mit() :提交事务
    • channel.txRollback() :回滚事务
  • Spring已经对上面三个方法进行了封装,所以我们只能使用原始的代码演示

3.1.1 生产者

public	class	Sender	{
public	static	void	main(String[]	args)	throws	Exception	{
Connection connection = ConnectionUtil.getConnection();
Channel	channel = connection.createChannel();

channel.exchangeDeclare("test_transaction","topic");
channel.txSelect();//开启事务
try	{
channel.basicPublish("test_transaction","product.price",null,"商品降价1".getBytes());
//System.out.println(1/0);//模拟异常!
channel.basicPublish("test_transaction","product.price",null,"商品降价2".getBytes());
System.out.println("消息全部发出!");
channel.txCommit();//事务提交
}catch	(Exception e){
System.out.println("由于系统异常,消息全部撤回!");
channel.txRollback();//事务回滚
e.printStackTrace();
}finally	{
channel.close();
connection.close();
}
}
}

3.1.2 消费者

public	class	Recer	{
public	static	void	main(String[]	args)	throws	Exception	{
Connection connection =ConnectionUtil.getConnection();
Channel	channel	=connection.createChannel();
channel.queueDeclare("test_transaction_queue",false,false,false,null);
channel.queueBind("test_transaction_queue","test_transaction",
"product.#");
DefaultConsumer	consumer=new DefaultConsumer(channel){
@Override
public	void	handleDelivery(String consumerTag,Envelope	envelope,
AMQP.BasicProperties properties, byte[]	body)	throws	IOException	{
String	s=new	String(body);
System.out.println("【消费者】="+s);
}
};
channel.basicConsume("test_transaction_queue",	true,consumer);
}
}

3.2 Confirm 发布确认机制

  • RabbitMQ为了保证消息的成功投递,采用通过AMQP协议层面为我们提供事务机制的方案,但是采用事务会大大降低消息的吞吐量

  • SSD硬盘测试结果10w条消息未开启事务,大约8s发送完毕;而开启了事务后,需要将近310s,差了30多倍。

  • 官方文档

Using standard AMQP 0-9-1, the only way to guarantee that a message isn’t lost is by using transactions – make the channel transactional then for each message or set of messages publish, com mit. In this case, transactions are unnecessarily heavyweight and decrease throughput by a factor of 250. To rem edy this, a confirm ation mechanism was introduced. It mim ics the consum er acknowledgem ents mechanism already present in the protocol.

  • 关键性译文:开启事务性能最大损失超过250倍那么有没有更加高效的解决方式呢?答案就是采用Confirm 模式。事务效率为什么会这么低呢?试想一下:10条消息,前9条成功,如果第10条失败,那么9条消息要全部撤销回滚。太太太浪费而confirm 模式则采用补发第10条的措施来完成10条消息的送达

3.2.1 在spring中应用

  • spring-rabbitmq-producer.xml
<!--1.配置连接,启动生产者确认机制:	publisher-confirms="true"-->
<rabbit:connection-factory	id="connectionFactory"
host="192.168.204.141"
port="5672"
username="root"
password="123123"
virtual-host="/root"
publisher-confirms="true"
/>
<!--6.配置rabbitmq的模版,添加确认回调处理类:confirm-
callback="msgSendConfirmCallback"-->
<rabbit:templateid="rabbitTemplate"
connection-factory="connectionFactory"
exchange="spring_topic_exchange"
message-converter="jsonMessageConverter"
confirm-callback="msgSendConfirmCallback"/>

<!--7.确认机制处理类-->
<beanid="msgSendConfirmCallback"class="confirm.MsgSendConfirmCallback"/>
  • 消息确认处理类
package confirm;


import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.stereotype.Component;
import java.io.IOException;

/**
* @BelongsProject: spring-rabbitmq-producer
* @Description: 确认机制
*/
@Component
public class MsgSendConfirmCallback implements
RabbitTemplate.ConfirmCallback {
public void confirm(CorrelationData correlationData, boolean b, String
s) {
if (b){
System.out.println("消息确认成功!!");
} else {
System.out.println("消息确认失败。。。");
// 如果本条消息一定要发送到队列中,例如下订单消息,我们可以采用消息补发
// 采用递归(固定次数,不可无限)或	redis+定时任务
}
}
}
  • log4j.properties
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %m%n

log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=rabbitmq.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %l
%m%n

log4j.rootLogger=debug, stdout,file
  • 发送消息
public	class	Sender	{
public	static	void	main(String[]	args)	{
ClassPathXmlApplicationContext	context	=new
ClassPathXmlApplicationContext("spring/spring-rabbitmq-producer.xml");
RabbitTemplate	rabbitTemplate	=context.getBean(RabbitTemplate.class);
Map<String,String>	map= new HashMap<String,	String>();
map.put("name",	"吕布");
map.put("email", "666@qq.com");
//第一个参数是路由名称,
//不写,则使用spring容器中创建的路由
//乱写一个,因为路由名错误导致报错,则进入消息确认失败流程
rabbitTemplate.convertAndSend("x","msg.user",map);
System.out.println("ok");
context.close();
}
}

4 消费端限流

  • Rabbitmq服务器积压了成千上万条未处理的消息,然后随便打开一个消费者客户端,就会突然宕机: 巨量的消息瞬间全部喷涌推送过来,但是单个客户端无法同时处理这么多数据,就会被压垮崩溃

  • 所以,当数据量特别大的时候,我们对生产端限流肯定是不科学的,因为有时候并发量就是特别大,有时候并发量又特别少,这是用户的行为,我们是无法约束的

  • 所以我们应该对消费端限流,用于保持消费端的稳定

  • RabbitMQ 提供了一种Qos (Quality of Service,服务质量)服务质量保证功能

    • 即在非自动确认消息的前提下,如果一定数目的消息未被确认前,不再进行消费新的消息
  • 生产者使用循环发出多条消息

for(int	i=1;i<=10;i++)	{
rabbitTemplate.convertAndSend("msg.user",map);
System.out.println("消息已发出...");
}

生产10条堆积未处理的消息

image.png

  • 消费者进行限流处理
<!--5.配置监听-->
<!--prefetch="3"	一次性消费的消息数量。会告诉	RabbitMQ	不要同时给一个消费者推送多于N个消息,一旦有N	个消息还没有ack,则该	consumer	将阻塞,直到消息被ack-->
<!--acknowledge-mode:	manual	手动确认-->
<rabbit:listener-container connection-factory="connectionFactory"
prefetch="3" acknowledge="manual">
<rabbit:listener ref="consumerListener"	queue-names="test_spring_queue_1"/>
</rabbit:listener-container>
//	AbstractAdaptableMessageListener用于在spring容器接收到消息后用于处理消息的抽象
基类
@Component
public	class	ConsumerListener	extends	AbstractAdaptableMessageListener{
//jackson提供序列化和反序列中使用最多的类,用来转换json的
private	static	final	ObjectMapper MAPPER = new	ObjectMapper();

public	void	onMessage(Message	message,	Channel	channel)	throws	Exception
{
try{
//String str = new String(message.getBody());
//将message对象转换成json
JsonNode jsonNode = MAPPER.readTree(message.getBody());
String	name=jsonNode.get("name").asText();
String	email=jsonNode.get("email").asText();
System.out.println("从队列中获取:【"+name+"的邮箱是:"+email+"】");
 
long	deliveryTag=message.getMessageProperties().getDeliveryTag();
//确认收到(参数1,参数2)
/*
参数1:RabbitMQ	向该	Channel	投递的这条消息的唯一标识	ID,是一个单调递
增的正整数,delivery_tag	的范围仅限于	Channel
参数2:为了减少网络流量,手动确认可以被批处理,当该参数为	true	时,则可以
一次性确认	delivery_tag	小于等于传入值的所有消息
*/
channel.basicAck(deliveryTag,true);
Thread.sleep(3000);
System.out.println("休息三秒然后再接收消息");
}	catch	(Exception e){
e.printStackTrace();
}
}
}

每次确认接收3条消息

image.png

5 过期时间TTL

  • Time To Live:生存时间、还能活多久,单位毫秒

  • 在这个周期内,消息可以被消费者正常消费,超过这个时间,则自动删除(其实是被称为dead message并投入到死信队列,无法消费该消息)

  • RabbitMQ可以对消息和队列设置TTL

    • 通过队列设置,队列中所有消息都有相同的过期时间
    • 对消息单独设置,每条消息的TTL可以不同(更颗粒化)

5.1 设置队列TTL

spring-rabbitmq-producer.xml

<!--2.重新配置一个队列,同时,对队列中的消息设置过期时间-->
<rabbit:queue	name="test_spring_queue_ttl"	auto-declare="true">
<rabbit:queue-arguments>
<entry	key="x-message-ttl" value-type="long"	value="5000"></entry>
</rabbit:queue-arguments>
</rabbit:queue>

image.png

5秒之后,消息自动删除

image.png

5.2 设置消息TTL

设置某条消息的ttl,只需要在创建发送消息时指定即可

<!-- 2. 配置队列 -->
<rabbit :queuename="test _spring_queue_ttl_2" >
package	test;


import	org.springframework.amqp.core.Message;
import	org.springframework.amqp.core.MessageProperties;
import	org.springframework.amqp.rabbit.core.RabbitTemplate;
import	org.springframework.context.support.ClassPathXmlApplicationContext;

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

/**
*	@BelongsProject:	spring-rabbitmq-producer
*	@Description:	生产者
*/
public	class	Sender2	{
public	static	void	main(String[]	args)	{
ClassPathXmlApplicationContext	context	=	new ClassPathXmlApplicationContext("spring/spring-rabbitmq-producer.xml");
RabbitTemplate	rabbitTemplate	=context.getBean(RabbitTemplate.class);
//创建消息配置对象
MessageProperties messageProperties = new	MessageProperties();
//设置消息过期时间
messageProperties.setExpiration("6000");
//创建消息
Message	message	= new	Message("6秒后自动删除".getBytes(),
messageProperties);
//发送消息
rabbitTemplate.convertAndSend("msg.user",message);
System.out.println("消息已发出...");
context.close();
}
}

如果同时设置了queue和message的TTL值,则二者中较小的才会起作用

6 死信队列

  • DLX(Dead Letter Exchanges)死信交换机/死信邮箱,当消息在队列中由于某些原因没有被及时消费而变成死信(dead message)后,这些消息就会被分发到DLX交换机中,而绑定DLX交换机的队列,称之为:“死信队列”

  • 消息没有被及时消费的原因:

    • 消息被拒绝(basic.reject/ basic.nack)并且不再重新投递 requeue=false
    • 消息超时未消费
    • 达到最大队列长度

image.png

  • spring-rabbitmq-producer-dlx.xml
<!--1.配置连接-->
<rabbit:connection-factory id="connectionFactory"
host="192.168.204.141"
port="5672"
username="root"
password="123123"
virtual-host="/root"/>
<!--3.配置rabbitAdmin:主要用于在java代码中对队列的管理,用来创建,绑定,删除队列与交
换机,发送消息等-->
<rabbit:admin	connection-factory="connectionFactory"/>
<!--6.配置rabbitmq的模版-->
<rabbit:template id="rabbitTemplate"
connection-factory="connectionFactory"
exchange="my_exchange"/>
<!--
############################################################################
##########################################-->
<!--死信队列-->
<rabbit:queuename="dlx_queue"/>
<!--定向死信交换机-->
<rabbit:direct-exchangename="dlx_exchange">
<rabbit:bindings>
<rabbit:bindingkey="dlx_ttl" queue="dlx_queue"></rabbit:binding>
<rabbit:bindingkey="dlx_max" queue="dlx_queue"></rabbit:binding>
</rabbit:bindings>
</rabbit:direct-exchange>

<!--测试超时的队列-->
<rabbit:queue name="test_ttl_queue">
<rabbit:queue-arguments>
<!--队列ttl为6秒-->
<entry key="x-message-ttl" value-type="long" value="6000"/>
<!--超时	消息	投递给	死信交换机-->
<entry key="x-dead-letter-exchange" value="dlx_exchange"/>
</rabbit:queue-arguments>
</rabbit:queue>

<!--测试超长度的队列-->
<rabbit:queue name="test_max_queue">
<rabbit:queue-arguments>
<!--队列ttl为6秒-->
<entry key="x-max-length"value-type="long"value="2"/>
<!--超时	消息	投递给	死信交换机-->
<entry key="x-dead-letter-exchange"value="dlx_exchange"/>
</rabbit:queue-arguments>
</rabbit:queue>

<!--定向测试交换机-->
<rabbit:direct-exchange name="my_exchange">
<rabbit:bindings>
<rabbit:binding key="dlx_ttl" queue="test_ttl_queue">
</rabbit:binding>
<rabbit:binding key="dlx_max" queue="test_max_queue">
</rabbit:binding>
</rabbit:bindings>
</rabbit:direct-exchange>
  • 发消息进行测试
public class SenderDLX{
public static void main(String[] args){
ClassPathXmlApplicationContext  context=new
ClassPathXmlApplicationContext("spring/spring-rabbitmq-producer-dlx.xml");
RabbitTemplate rabbitTemplate=
context.getBean(RabbitTemplate.class);
//rabbitTemplate.convertAndSend("dlx_ttl","测试超时".getBytes());
rabbitTemplate.convertAndSend("dlx_max","测试长度1".getBytes());
rabbitTemplate.convertAndSend("dlx_max","测试长度2".getBytes());
rabbitTemplate.convertAndSend("dlx_max","测试长度3".getBytes());
System.out.println("消息已发出...");
context.close();
}
}

7 延迟队列

  • 延迟队列:TTL + 死信队列的合体

  • 死信队列只是一种特殊的队列,里面的消息仍然可以消费

  • 在电商开发部分中,都会涉及到延时关闭订单,此时延迟队列正好可以解决这个问题

7.1 生产者

沿用上面死信队列案例的超时测试,超时时间改为订单关闭时间即可

7.2 消费者

spring-rabbitmq-consumer.xml

<!--	监听死信队列	-->
<rabbit:listener-container connection-factory="connectionFactory"	prefetch="3"
acknowledge="manual">
<rabbit:listener ref="consumerListener"	queue-names="dlx_queue"	/>
</rabbit:listener-container>