- 在使用Rabbit时,可能会遇到投递失败的情况,所以我门需要知道消息是否发送成功。
1.1Confirm消息确认机制:
如何实现Confirm确认消息?
第一步:在channel上开启确认模式:channel.confirmSelect()
第二步:在channel上添加监听:addConfirmListener,监听成功和失败的返回结果,
根据具体的结果对消息进行重新发送、或者记录日志等后续处理。
rabbitmq 整个消息投递的路径为:
producer—>rabbitmq broker—>exchange—>queue—>consumer
消息从 producer 到 exchange 则会返回一个 confirmCallback 。
消息从 exchange–>queue 投递失败则会返回一个 returnCallback 。
我们将利用这两个 callback 控制消息的可靠性投递。
1.2:使用Spring实现:
配置省略。
Producer开启确认模式:
1,在ConnectionFactory中设置 publisher-confirms=“true”
2,给RabbitTemplate设置ConfirmCallback回调函数。
代码:
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring_rabbitmq.xml")
public class Test {
@Autowired
private RabbitTemplate rabbitTemplate;
@org.junit.Test
public void test1()throws Exception{
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback(){
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack){
System.out.println("成功");
}else {
System.out.println("失败"+cause);
}
}
});
rabbitTemplate.convertAndSend("spring_exchange_confirm","confirm","鲨鱼的手臂");
Thread.sleep(100);
}
ConSumer代码:
<rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual">
<rabbit:listener ref="springConsumer" queue-names="spring_queue_confirm"></rabbit:listener>
</rabbit:listener-container>
设置确认消息的方式为手动acknowledge=“manual”
添加ChannelAwareMessageListener监听
@Component
public class springConsumer implements ChannelAwareMessageListener {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
System.out.println(new String(message.getBody()));
channel.basicAck(deliveryTag,true);
}catch (Exception e){
e.printStackTrace();
System.out.println("错了吧");
channel.basicNack(deliveryTag,true,true);
}
}
}
使用自动确认,在实际开发中可能会出现,消息被确认接收之后但是业务代码出现了问题,消息就会丢失。但是使用手动确认,如果业务代码出现了问题,可以调用channel.basicNack()方法,让其自动重新发送消息。
- 回退机制:return listener
当我门无法将消息推送到队列,就需要监听这种不可达消息。
代码:
Producer代码:
@org.junit.Test
public void test2()throws Exception{
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println(replyText);
System.out.println(new String(message.getBody()));
System.out.println(replyCode);
}
});
rabbitTemplate.convertAndSend("spring_exchange_confirm","confirmss","鲨鱼的手臂2");
Thread.sleep(100);
}
配置文件中的ConnectionFactory 设置publisher-returns=“true”,开启回退机制。
测试代码中设置 rabbitTemplate.setMandatory(true);
(设置Exchange处理消息的模式:默认为false,表示如果没有到达队列就会将消息舍弃,设置为true,表示如果没有到达队列会将消息发送到ReturnCallBack)。
- Consumer限流机制:确保ack机制为manual(手动确认机制)
Consumer端配置文件
在监听器添加prefetch=“1”(表示每次只取一个)
<rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual" prefetch="1">
<rabbit:listener ref="springConsumer" queue-names="spring_queue_confirm"></rabbit:listener>
</rabbit:listener-container>
- TTL(Time To Live):过期时间:
Producer将消息推送至队列中,超过时间没有被Consumer端取出,就会被自动清除。(应用于订单支付)
代码:
Producer配置文件:
<rabbit:queue id="spring_queue_confirm_ttl" name="spring_queue_confirm_ttl" auto-declare="true">
<rabbit:queue-arguments>
<entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"></entry>
</rabbit:queue-arguments>
</rabbit:queue>
<rabbit:topic-exchange name="spring_exchange_confirm_ttl" auto-declare="true">
<rabbit:bindings>
<rabbit:binding pattern="ttl.*" queue="spring_queue_confirm_ttl"></rabbit:binding>
</rabbit:bindings>
</rabbit:topic-exchange>
给队列设置参数:x-message-ttl(注意参数不是随意设置具体参考RabbitMQ后台客户端。)
表示设置过期时间为10s。
Producer测试代码:
@org.junit.Test
public void test4() throws Exception {
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println(new String(message.getBody()));
System.out.println(replyCode);
System.out.println(replyText);
System.out.println(exchange);
System.out.println(routingKey);
}
});
rabbitTemplate.convertAndSend("spring_exchange_confirm_ttl", "ttl.huhu", "鲨鱼的手臂4");
Thread.sleep(100);
}
- 死信队列DeadLetter Exchange(DLE)
6.1当消息消息在队列中到了过期时间,会发送到另一个交换机上,这个交换机被称为死信队列。
6.2消息成为死信的三种情况:
1,到了存活时间消息未被消费;
2,消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
3,队列消息长度到达限制;
<!--`````````````````创建死信队列``````````````````-->
<rabbit:queue id="spring_test_queue_dlx" name="spring_test_queue_dlx" auto-declare="true">
<rabbit:queue-arguments>
<entry key="x-dead-letter-exchange" value="spring_exchange_dlx"></entry>
<entry key="x-dead-letter-routing-key" value="dlx.ddd"></entry>
<entry key="x-max-length" value="10" value-type="java.lang.Integer"></entry>
</rabbit:queue-arguments>
</rabbit:queue>
<rabbit:topic-exchange name="spring_test_exchange_dlx" auto-declare="true">
<rabbit:bindings>
<rabbit:binding pattern="dlx.*" queue="spring_test_queue_dlx"></rabbit:binding>
</rabbit:bindings>
</rabbit:topic-exchange>
<rabbit:queue id="spring_queue_dlx" name="spring_queue_dlx" auto-declare="true">
</rabbit:queue>
<rabbit:topic-exchange name="spring_exchange_dlx" auto-declare="true">
<rabbit:bindings>
<rabbit:binding pattern="dlx.*" queue="spring_queue_dlx"></rabbit:binding>
</rabbit:bindings>
</rabbit:topic-exchange>
解析:创建两个正常队列跟交换机,把一个交换机跟队列作为死信交换机跟死信队列。
正常队列绑定死信交换机:
<rabbit:queue-arguments>
<entry key="x-dead-letter-exchange" value="spring_exchange_dlx"></entry>
<entry key="x-dead-letter-routing-key" value="dlx.ddd"></entry>
<entry key="x-max-length" value="10" value-type="java.lang.Integer"></entry>
</rabbit:queue-arguments>
配置表示如果队列长度到10,后面的消息就会进入死信交换机。
producer测试代码:
@org.junit.Test
public void test5()throws Exception{
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println(new String(message.getBody()));
System.out.println(replyCode);
System.out.println(replyText);
System.out.println(exchange);
System.out.println(routingKey);
}
});
for (int i = 0; i <20; i++) {
rabbitTemplate.convertAndSend("spring_test_exchange_dlx", "dlx.huhu", "鲨鱼的手臂5");
}
7.延时队列
例如用于订单系统中超过30分钟就会取消订单。
由于Rabbit中没有提供延时队列功能,我们可以采取TTL+死信队列组合的方式实现。
代码:
Producer配置文件:
<!--````````````````创建延时队列`````````````````````-->
<rabbit:queue id="spring_test_queue_dlx_TTL" name="spring_test_queue_dlx_TTL" auto-declare="true">
<rabbit:queue-arguments>
<entry key="x-dead-letter-exchange" value="spring_exchange_dlx_TTL"></entry>
<entry key="x-dead-letter-routing-key" value="dlx.ddd"></entry>
<entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"></entry>
</rabbit:queue-arguments>
</rabbit:queue>
<rabbit:topic-exchange name="spring_test_exchange_dlx" auto-declare="true">
<rabbit:bindings>
<rabbit:binding pattern="dlx.*" queue="spring_test_queue_dlx_TTL"></rabbit:binding>
</rabbit:bindings>
</rabbit:topic-exchange>
<rabbit:queue id="spring_queue_dlx_TTL" name="spring_queue_dlx_TTL" auto-declare="true">
</rabbit:queue>
<rabbit:topic-exchange name="spring_exchange_dlx_TTL" auto-declare="true">
<rabbit:bindings>
<rabbit:binding pattern="dlx.*" queue="spring_queue_dlx_TTL"></rabbit:binding>
</rabbit:bindings>
</rabbit:topic-exchange>
Consumer配置文件代码:
<rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual">
<rabbit:listener ref="springConsumer" queue-names="spring_queue_dlx_TTL"></rabbit:listener>
</rabbit:listener-container>
注意的是,延时队列的效果一定要监听死信队列。
- RabbitAdmin的应用:
① 底层实现就是从Spring容器中获取Exchange,Bingding,Routingkey以及Queue的@bean声明
② 使用RabbitAdmin的execute方法执行对应的声明,修改和删除等一系列MQ的操作者
比如:添加一个交换机,删除一个绑定, 清空一个队列里面的消息等等
代码演示:
新建一个项目pom文件起步配置:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.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</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
新建程序入口:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
新建配置类
@Configuration
@ComponentScan({"com.xxx.*"})
public class RabbitMQConfig {
@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setAddresses("localhost:5672");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("my_vhost");
return connectionFactory;
}
@Bean
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
rabbitAdmin.setAutoStartup(true);
return rabbitAdmin;
}
}
新建测试类
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {
@Autowired
private RabbitAdmin rabbitAdmin;
@Test
public void test1(){
//声明交换机
rabbitAdmin.declareExchange(new DirectExchange("springboot.test.direct",true,false));
rabbitAdmin.declareExchange(new DirectExchange("springboot.test.topic",true,false));
rabbitAdmin.declareExchange(new DirectExchange("springboot.test.fanout",true,false));
//声明队列
rabbitAdmin.declareQueue(new Queue("springboot.test.direct.queue",true));
rabbitAdmin.declareQueue(new Queue("springboot.test.topic.queue",true));
rabbitAdmin.declareQueue(new Queue("springboot.test.fanout.queue",true));
//交换机与队列进行绑定
rabbitAdmin.declareBinding(BindingBuilder.bind(new Queue("springboot.test.direct.queue",true)).to(new DirectExchange("springboot.test.direct",true,false)).with("direct"));
rabbitAdmin.declareBinding(BindingBuilder.bind(new Queue("springboot.test.topic.queue",true)).to(new DirectExchange("springboot.test.topic",true,false)).with("user.#"));
rabbitAdmin.declareBinding(
BindingBuilder.bind(new Queue("springboot.test.fanout.queue",true))
.to(new FanoutExchange("springboot.test.fanout",true,false))
);
}
}
使用 SpringAMQP 去声明,就需要使用SpringAMQP 的如下模式,即声明 Bean 方式
/**
* 针对消费者配置
* 1. 设置交换机类型
* 2. 将队列绑定到交换机
FanoutExchange: 将消息分发到所有的绑定队列,无routingkey的概念
HeadersExchange :通过添加属性key-value匹配
DirectExchange:按照routingkey分发到指定队列
TopicExchange:多关键字匹配
*/
@Bean
public TopicExchange exchange001() {
return new TopicExchange("topic001", true, false);
}
@Bean
public Queue queue001() {
return new Queue("queue001", true); //队列持久
}
@Bean
public Binding binding001() {
return BindingBuilder.bind(queue001()).to(exchange001()).with("spring.*");
}
@Bean
public TopicExchange exchange002() {
return new TopicExchange("topic002", true, false);
}
@Bean
public Queue queue002() {
return new Queue("queue002", true); //队列持久
}
@Bean
public Binding binding002() {
return BindingBuilder.bind(queue002()).to(exchange002()).with("rabbit.*");
}
@Bean
public Queue queue003() {
return new Queue("queue003", true); //队列持久
}
@Bean
public Binding binding003() {
return BindingBuilder.bind(queue003()).to(exchange001()).with("mq.*");
}
@Bean
public Queue queue_image() {
return new Queue("image_queue", true); //队列持久
}
@Bean
public Queue queue_pdf() {
return new Queue("pdf_queue", true); //队列持久
}
- RabbitMQ与SpringBoot整合
Producer:
pom文件起步依赖:
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<dependencies>
<!--2. rabbitmq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
yml配置文件代码
spring:
rabbitmq:
host: 139.196.37.163
port: 5672
username: admin
password: admin
virtual-host: my_vhost
配置类:
public static final String EXCHANGE_NAME = "boot_topic_exchange";
public static final String QUEUE_NAME = "boot_queue";
//交换机
@Bean
public Exchange bootExchange(){
return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
}
//队列
@Bean
public Queue bootQueue(){
return QueueBuilder.durable(QUEUE_NAME).build();
}
//绑定交换机与队列
//noargs():表示不指定参数
@Bean
public Binding bootBinding(Queue bootQueue,Exchange bootExchange){
return BindingBuilder.bind(bootQueue).to(bootExchange).with("boot.*").noargs();
}
测试类(发送消息):
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void test1(){
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"boot.haha","鲨鱼的手臂");
}
}
Consumer(消费者代码)
pom.xml,yml配置文件跟Producer一样。
监听器类:
@Component
public class RabbimtMQListener{
@RabbitListener(queues = "boot_queue")
public void listBootQueue(Message message){
System.out.println(new String(message.getBody()));
}
}
- 百分百消息投递
我们在工作时难免会遇到消息投递,而且需要保证消息不能丢失。
如图所示,在发送消息时,不像之前,直接编辑好消息就发送出去,在发送消息出去前做一些操作。
Step 1: 首先把消息信息(业务数据)存储到数据库中,紧接着,我们再把这个消息记录也存储到一张消息记录表里(或者另外一个同源数据库的消息记录表)
Step 2:发送消息到MQ Broker节点(采用confirm方式发送,会有异步的返回结果)
Step 3、4:生产者端接受MQ Broker节点返回的Confirm确认消息结果,然后进行更新消息记录表里的消息状态。比如默认Status = 0 当收到消息确认成功后,更新为1即可!
Step 5:但是在消息确认这个过程中可能由于网络闪断、MQ Broker端异常等原因导致 回送消息失败或者异常。这个时候就需要发送方(生产者)对消息进行可靠性投递了,保障消息不丢失,100%的投递成功!(有一种极限情况是闪断,Broker返回的成功确认消息,但是生产端由于网络闪断没收到,这个时候重新投递可能会造成消息重复,需要消费端去做幂等处理)所以我们需要有一个定时任务,(比如每5分钟拉取一下处于中间状态的消息,当然这个消息可以设置一个超时时间,比如超过1分钟 Status = 0 ,也就说明了1分钟这个时间窗口内,我们的消息没有被确认,那么会被定时任务拉取出来)
Step 6:接下来我们把中间状态的消息进行重新投递 retry send,继续发送消息到MQ ,当然也可能有多种原因导致发送失败
Step 7:我们可以采用设置最大努力尝试次数,比如投递了3次,还是失败,那么我们可以将最终状态设置为Status = 2 ,最后 交由人工解决处理此类问题(或者把消息转储到失败表中)。
-- ----------------------------
-- Table structure for broker_message_log
-- ----------------------------
DROP TABLE IF EXISTS `broker_message_log`;
CREATE TABLE `broker_message_log` (
`message_id` varchar(255) NOT NULL COMMENT '消息唯一ID',
`message` varchar(4000) NOT NULL COMMENT '消息内容',
`try_count` int(4) DEFAULT '0' COMMENT '重试次数',
`status` varchar(10) DEFAULT '' COMMENT '消息投递状态 0投递中,1投递成功,2投递失败',
`next_retry` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP COMMENT '下一次重试时间',
`create_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for t_order
-- ----------------------------
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`message_id` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2018091102 DEFAULT CHARSET=utf8;
- 消息幂等性
幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。
比如我们扫码付款时,由于网卡,付款失败后又扫了一次码(码没有刷新),这样多次扫,我们会担心会不会多次扣款,但是设置消息幂等性后就不会出现多次扣款的事情。
消息幂等性保障 乐观锁机制。
我们可以在提交付款数据时添加一个版本属性。
例如:`
id=1,money=500,version=1
消费者接收到
id=1,money=500,version=1
第一次执行SQL语句
第一次执行:version=1
update account set money = money - 500 , version = version + 1
where id = 1 and version = 1
第二次执行:version=2
第二次执行:version=2
update account set money = money - 500 , version = version + 1
where id = 1 and version = 1