引言
一篇带你立马搞定消息中间件,并使用RabbitMQ模拟用户下单,外卖小哥抢单的情景。 这篇文章有点长哦,希望你耐着性子看完,然后会有所收获!!!!
背景
时间回到2003年,一群开源开发者集合在一起形成了Apache Geronimo。之后,他们发现当前没有好用的使用BSD-style许可协议的消息代理器。Geronimo是由于java EE兼容性需要一个JMS实现。所以一些开发者开始讨论其可能性。拥有丰富MOMs经验甚至自己创建过一些MOMs的这些开发者开始创建下一个伟大的开源消息代理。ActiveMQ这么快开始是因为当时市场上大多数的MOMs是商业,闭源而且购买和支持昂贵。市场上的MOMs已经广泛地被使用,但是一些商业行为是买不起如此昂贵的软件。这使得创建一个开源MOMs的需求更加大。很明显,有一个市场急需一个开源的使用Apache License的MOM。最终就导致了Apache ActiveMQ的诞生。
ActiveMQ遵循JMS规范,是为分布式应用远程交流而创建的。为了理解这目的,最好就是去看一些分布式应用的设计和是交互。
导航
为什么使用MQ?MQ的优缺点
优点如下
-
解耦
:A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要这个数据呢?那如果 C 系统现在不需要了呢?A 系统负责人几乎崩溃…A 系统跟其它各种乱七八糟的系统严重耦合,A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。如果使用 MQ,A 系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况。
就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦。 -
异步
:A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求。如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms。 -
削峰
:减少高峰时期对服务器压力。
缺点如下
-
系统可用性降低
:本来系统运行好好的,现在你非要加入个消息队列进去,那消息队列挂了,你的系统不是呵呵了。因此,系统可用性会降低。 -
系统复杂度提高
: 加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。因此,需要考虑的东西更多,复杂性增大。 -
一致性问题
:A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。
心得
所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,做好之后,你会发现,妈呀,系统复杂度提升了一个数量级,也许是复杂了 10 倍。但是关键时刻,用,还是得用的。
Kafka、ActiveMQ、RabbitMQ、RocketMQ 的区别
ActiveMQ | RabbitMQ | RocketMQ | Kafka | ZeroMQ | |
---|---|---|---|---|---|
单机吞吐量 | 比RabbitMQ低 | 2.6w/s(消息做持久化) | 11.6w/s | 17.3w/s | 29w/s |
开发语言 | Java | Erlang | Java | Scala/Java | C |
主要维护者 | Apache | Mozilla/Spring | Alibaba | Apache | iMatix,创始人已去世 |
成熟度 | 成熟 | 成熟 | 开源版本不够成熟 | 比较成熟 | 只有C、PHP等版本成熟 |
订阅形式 | 点对点(p2p)、广播(发布-订阅) | 提供了4种:direct, topic ,Headers和fanout。fanout就是广播模式 | 基于topic/messageTag以及按照消息类型、属性进行正则匹配的发布订阅模式 | 基于topic以及按照topic进行正则匹配的发布订阅模式 | 点对点(p2p) |
持久化 | 支持少量堆积 | 支持少量堆积 | 支持大量堆积 | 支持大量堆积 | 不支持 |
顺序消息 | 不支持 | 不支持 | 支持 | 支持 | 不支持 |
性能稳定性 | 好 | 好 | 一般 | 较差 | 很好 |
集群方式 | 支持简单集群模式,比如’主-备’,对高级集群模式支持不好。 | 支持简单集群,'复制’模式,对高级集群模式支持不好。 | 常用 多对’Master-Slave’ 模式,开源版本需手动切换Slave变成Master | 天然的‘Leader-Slave’无状态集群,每台服务器既是Master也是Slave | 不支持 |
管理界面 | 一般 | 较好 | 一般 | 无 | 无 |
综上所述
综上,各种对比之后,有如下建议:
一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了;
后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高;
不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 Apache,但 GitHub 上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。
所以中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。
如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。
RabbitMQ的工作模式
首先先介绍一个简单的一个消息推送到接收的流程,提供一个简单的图:
黄色的圈圈就是我们的消息推送服务,将消息推送到 中间方框里面也就是 rabbitMq的服务器,然后经过服务器里面的交换机、队列等各种关系(后面会详细讲)将数据处理入列后,最终右边的蓝色圈圈消费者获取对应监听的消息。
一、simple模式(即最简单的收发模式)
1.消息产生消息,将消息放入队列
2.消息的消费者(consumer) 监听 消息队列,如果队列中有消息,就消费掉,消息被拿走后,自动从队列中删除(隐患 消息可能没有被消费者正确处理,已经从队列中消失了,造成消息的丢失,这里可以设置成手动的ack,但如果设置成手动ack,处理完后要及时发送ack消息给队列,否则会造成内存溢出)。
二、work工作模式(资源的竞争)
1.消息产生者将消息放入队列消费者可以有多个,消费者1,消费者2同时监听同一个队列,消息被消费。C1 C2共同争抢当前的消息队列内容,谁先拿到谁负责消费消息(隐患:高并发情况下,默认会产生某一个消息被多个消费者共同使用,可以设置一个开关(syncronize) 保证一条消息只能被一个消费者使用)。
三、publish/subscribe发布订阅(共享资源)
1、每个消费者监听自己的队列;
2、生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息。
四、routing路由模式
1.消息生产者将消息发送给交换机按照路由判断,路由是字符串(info) 当前产生的消息携带路由字符(对象的方法),交换机根据路由的key,只能匹配上路由key对应的消息队列,对应的消费者才能消费消息;
2.根据业务功能定义路由字符串;
3.从系统的代码逻辑中获取对应的功能字符串,将消息任务扔到对应的队列中。
4.业务场景:error 通知;EXCEPTION;错误通知的功能;传统意义的错误通知;客户通知;利用key路由,可以将程序中的错误封装成消息传入到消息队列中,开发者可以自定义消费者,实时接收错误;
五、topic 主题模式(路由模式的一种)
1.星号井号代表通配符
2.星号代表多个单词,井号代表一个单词
3.路由功能添加模糊匹配
4.消息产生者产生消息,把消息交给交换机
5.交换机根据key的规则模糊匹配到对应的队列,由队列的监听消费者接收消息消费;
(在我的理解看来就是routing查询的一种模糊匹配,就类似sql的模糊查询方式)
RabbitMQ的交换机类型
常用的交换机有以下三种,因为消费者是从队列获取信息的,队列是绑定交换机的(一般),所以对应的消息推送/接收模式也会有以下几种:
Direct Exchange
一、直连型交换机
根据消息携带的路由键将消息投递给对应队列。
大致流程,有一个队列绑定到一个直连交换机上,同时赋予一个路由键 routing key 。
然后当一个消息携带着路由值为X,这个消息通过生产者发送给交换机时,交换机就会根据这个路由值X去寻找绑定值也是X的队列。
二、Fanout Exchange
扇型交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。 这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。
三、Topic Exchange
主题交换机,这个交换机其实跟直连交换机流程差不多,但是它的特点就是在它的路由键和绑定键之间是有规则的。
简单地介绍下规则:
* (星号) 用来表示一个单词 (必须出现的)
# (井号) 用来表示任意数量(零个或多个)单词
通配的绑定键是跟队列进行绑定的,举个小例子
队列Q1 绑定键为 .TT. 队列Q2绑定键为 TT.#
如果一条消息携带的路由键为 A.TT.B,那么队列Q1将会收到;
如果一条消息携带的路由键为TT.AA.BB,那么队列Q2将会收到;
主题交换机是非常强大的,为啥这么膨胀?
当一个队列的绑定键为 “#”(井号) 的时候,这个队列将会无视消息的路由键,接收所有的消息。
当 * (星号) 和 # (井号) 这两个特殊字符都未在绑定键中出现的时候,此时主题交换机就拥有的直连交换机的行为。
所以主题交换机也就实现了扇形交换机的功能,和直连交换机的功能。
四、Dead Letter Exchange 死信交换机
先来看下架构图:
关于死信交换机的知识可以参考这篇博客 【RabbitMQ】一文带你搞定RabbitMQ死信队列.,保证你以下反应,绝对真实。。。。
PS:需要注意的是:需要将消费者的应答模式修改为手动应答模式,在application.yml中配置
acknowledge-mode: manual
,如果消费过程没有出现异常,那么调用channel.basicAck()
手动应答消息,否则channel.basicNack(deliveryTag, true, false);
丢弃消息。
另外还有 Header Exchange 头交换机
,Default Exchange 默认交换机
,其他两种就不暂不做描述了。
生产者的消息确认机制
问题产生背景
生产者发送消息出去之后,不知道到底有没有发送到RabbitMQ服务器, 默认是不知道的。而且有的时候我们在发送消息之后,后面的逻辑出问题了,我们不想要发送之前的消息了,需要撤回该怎么做??或者如何确保消息是否成功发送出去的??
解决方案:
1.AMQP 事务机制
2.Confirm 模式
一、AMQP 事务机制
前提是要将channel设置为事务模式,首先要调用channel.txSelect()
开启事务,然后在发送消息后如果出现异常,那么调用channel.txRollback()
回滚事务,否则调用channel.txCommit()
提交事务。
/**
*
* AMQP消息确认机制:
* 根据交换机名称和路由键精确匹配到对应的消息队列
* @param exchangeName 交换机名称
*/
public void sendForNotScheduledForAMQP(String exchangeName) throws Exception{
JSONObject jsonObject = new JSONObject();
//TOPIC:一对多,即一个消息发送者可以被多个消息接收者监听
String userName = System.currentTimeMillis() + "";
try {
jsonObject.put("userName", userName);
jsonObject.put("toEmail", "2590392428@qq.com");
} catch (JSONException e) {
e.printStackTrace();
}
String msg = jsonObject.toString();
String messageId=UUID.randomUUID() + "";
Message message = MessageBuilder.withBody(msg.getBytes()).setContentType(MessageProperties.CONTENT_TYPE_JSON)
.setContentEncoding("utf-8").setMessageId(messageId).build();
// 1、获取连接
Connection connection =null;
Channel channel =null;
try {
connection=connectionFactory.createConnection();
// 2、创建通道
channel=connection.createChannel(true);
// 3.创建交换机声明
//channel.exchangeDeclare(DirectRabbitConfig.EMAIL_QUEUE_NAME, true, false, false, null);
// 4、将当前管道设置为 txSelect 将当前channel设置为transaction模式 开启事务
channel.txSelect();
//5、发送消息,该操作是异步的
channel.basicPublish(exchangeName, DirectRabbitConfig.EMAIL_ROUTING_KEY,true,null,msg.getBytes());
String toEmail = "2590392428@qq.com";
// 请求地址
String emailUrl = "http://127.0.0.1:9004/email/getEmail?toEmail=" + toEmail;
PrintUtils.print(this.getClass(), "->>sendForNotScheduledForAMQP()->>(direct交换机)邮件消费者开始调用第三方邮件服务器,emailUrl:" + emailUrl);
String result = HttpClientUtils.doGet(emailUrl);
if(result==null){
// PS:如果调用第三方邮件接口无法访问,手动抛出异常,从而导致回滚事务
throw new Exception("调用第三方邮件服务器接口失败!");
}
PrintUtils.print(this.getClass(), "->>sendForNotScheduledForAMQP()->>(direct交换机)邮件消费者结束调用第三方邮件服务器成功,result:" + result + "程序执行结束");
//6、没有出现异常, 提交事务
PrintUtils.print(this.getClass(),"->>sendForNotScheduledForAMQP()->>没有出现异常, 提交事务");
channel.txCommit();
} catch (Exception e) {
e.printStackTrace();
PrintUtils.print(this.getClass(), "->>sendForNotScheduledForAMQP()->>出现异常,回滚事务");
//7、出现异常,回滚事务
channel.txRollback();
//sendForNotScheduledForAMQP(DirectRabbitConfig.EXCHANGE_NAME);
}
}
事务模式:
txSelect 将当前channel设置为transaction模式
txCommit 提交当前事务
txRollback 事务回滚
二、Confirm 模式
2、1 首先需要在rabbitmq-provider项目的application.yml文件上,加上消息确认的配置项后
ps: 本篇文章使用springboot版本为 2.1.7.RELEASE ;
如果你们在配置确认回调,测试发现无法触发回调函数,那么存在原因也许是因为版本导致的配置项不起效,可以把publisher-confirms: true
替换为 publisher-confirm-type: correlated
配置如下:
server:
port: 9005
spring:
#配置rabbitMq 服务器
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest #登录用户名(默认的,也可以进行添加用户)
password: guest #登录密码(默认的,也可以进行修改)
#虚拟host 可以不设置,使用server默认host
virtual-host: /hkm
#消息确认配置项->>注意:如果使用AMQP模式的消息确认机制,则需要注释掉以下配置
#开启 confirm 确认机制,确认消息发送到交换机(Exchange),是否确定回调
publisher-confirm-type: correlated
#开启 return 确认机制,确认消息发送到队列(Queue),是否确定回调
publisher-returns: true
#设置为 true后消费者在消息没有被路由到合适队列情况下会被return监听,而不会自动删除
template:
mandatory: true
listener:
simple:
retry:
####开启消费者异常重试
enabled: true
####最大重试次数
max-attempts: 5
####重试间隔次数
initial-interval: 5000
####默认为自动应答,开启手动ack,只要cosumer没有手动ack,那么消息会一直存在消息队列中,只有手动ack了,才会将消息从消息队列中删除掉
acknowledge-mode: manual
default-requeue-rejected: false
boot:
admin:
notify:
mail:
#配置是否启用邮件通知 false是不起用
enabled: true
##方法一:增加超时时间(单位:ms)
monitor:
read-timeout: 30000
2、2 需要编写一个类实现RabbitTemplate.ConfirmCallback
和RabbitTemplate.ReturnCallback
并重写方法或者使用匿名内部类,回调函数也可以。
package com.sprjjs.rabbitmq.config;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author HKM
* 消息确认触发回调函数的情况:
* ①消息推送到server,但是在server里找不到交换机
* 结论: ①这种情况触发的是 ConfirmCallback 回调函数。
* ②消息推送到server,找到交换机了,但是没找到队列
* 结论:②这种情况触发的是 ConfirmCallback和RetrunCallback两个回调函数。
* ③消息推送到sever,交换机和队列啥都没找到
* 结论: ③这种情况触发的是 ConfirmCallback 回调函数。
* ④消息推送成功
* 结论: ④这种情况触发的是 ConfirmCallback 回调函数。
*/
@Component
public class RabbitConfirmAndReturn implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback{
/**
* 确认消息是否正确发送到交换机(Exchange)回调的函数
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("ConfirmCallback: "+"消息id:"+correlationData.getId());
System.out.println("ConfirmCallback: "+"相关数据:"+correlationData);
System.out.println("ConfirmCallback: "+"确认情况:"+ack);
System.out.println("ConfirmCallback: "+"原因:"+cause);
//当找到了交换机的情况返回true,否则返回false
if (ack) {
System.out.println("消息发送确认成功");
} else {
System.out.println("消息发送确认失败:" + cause);
//使用确认机制,一直发送,直到消息发送确认成功
//System.out.println("ConfirmCallback: "+"producerEmailDirect:"+producerEmailDirect);
//producerEmailDirect.sendForNotScheduled(DirectRabbitConfig.EXCHANGE_NAME);
}
}
/**
* 确认消息是否正确发送到队列(Queue)回调的函数
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("ReturnCallback: "+"消息:"+message);
System.out.println("ReturnCallback: "+"回应码:"+replyCode);
System.out.println("ReturnCallback: "+"回应信息:"+replyText);
System.out.println("ReturnCallback: "+"交换机:"+exchange);
System.out.println("ReturnCallback: "+"路由键:"+routingKey);
}
}
2、3 发送者发送消息时,需要调用以下api
代码如下:
package com.sprjjs.rabbitmq.producer.direct;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.sprjjs.rabbitmq.config.RabbitConfirmAndReturn;
import com.sprjjs.rabbitmq.config.direct.DirectRabbitConfig;
import java.util.UUID;
@Component
public class ProducerSmsDirect {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RabbitConfirmAndReturn rabbitConfirmAndReturn;
@Autowired
private ConnectionFactory connectionFactory;
/**
* confirm消息确认机制:
* 根据交换机名称和路由键精确匹配到对应的消息队列
* @param exchangeName 交换机名称
*/
public void sendForNotScheduledForConfirm(String exchangeName){
JSONObject jsonObject = new JSONObject();
//TOPIC:一对多,即一个消息发送者可以被多个消息接收者监听
try {
jsonObject.put("toPhone", "13711769935");
} catch (JSONException e) {
e.printStackTrace();
}
String msg = jsonObject.toString();
String messageId=UUID.randomUUID() + "";
Message message = MessageBuilder.withBody(msg.getBytes()).setContentType(MessageProperties.CONTENT_TYPE_JSON)
.setContentEncoding("utf-8").setMessageId(messageId).build();
// 构建回调返回的数据
CorrelationData correlationData = new CorrelationData(messageId);
//设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
rabbitTemplate.setMandatory(true);
//设置确认消息是否发送到交换机(Exchange)回调函数
rabbitTemplate.setConfirmCallback(rabbitConfirmAndReturn);
//设置确认消息是否发送到队列(Queue)回调函数
rabbitTemplate.setReturnCallback(rabbitConfirmAndReturn);
//PS:发送消息,该操作是异步的
rabbitTemplate.convertAndSend(exchangeName, DirectRabbitConfig.SMS_ROUTING_KEY, message,correlationData);
}
}
不近视的小伙伴应该可以发现上面写了两个回调函数,一个叫 RabbitTemplate.ConfirmCallback
,一个叫 RabbitTemplate.RetrunCallback
;那么以上这两种回调函数都是在什么情况会触发呢?
先从总体的情况分析,推送消息存在四种情况:
①消息推送到server,但是在server里找不到交换机
②消息推送到server,找到交换机了,但是没找到队列
③消息推送到sever,交换机和队列啥都没找到
④消息推送成功
那么我先写几个接口来分别测试和验证下以上4种情况,消息确认触发回调函数的情况:
①消息推送到server,但是在server里找不到交换机写个测试接口,把消息推送到名为‘non-existent-exchange’的交换机上(这个交换机是没有创建没有配置的):
@GetMapping("/TestMessageAck")
public String TestMessageAck() {
String messageId = String.valueOf(UUID.randomUUID());
String messageData = "message: non-existent-exchange test message ";
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Map<String, Object> map = new HashMap<>();
map.put("messageId", messageId);
map.put("messageData", messageData);
map.put("createTime", createTime);
rabbitTemplate.convertAndSend("non-existent-exchange", "TestDirectRouting", map);
return "ok";
}
调用接口,查看rabbitmq-provuder项目的控制台输出情况(原因里面有说,没有找到交换机’non-existent-exchange’):
2019-09-04 09:37:45.197 ERROR 8172 --- [ 127.0.0.1:5672] o.s.a.r.c.CachingConnectionFactory : Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'non-existent-exchange' in vhost 'JCcccHost', class-id=60, method-id=40)
ConfirmCallback: 相关数据:null
ConfirmCallback: 确认情况:false
ConfirmCallback: 原因:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'non-existent-exchange' in vhost 'JCcccHost', class-id=60, method-id=40)
结论:①这种情况触发的是 ConfirmCallback 回调函数。
②消息推送到server,找到交换机了,但是没找到队列
这种情况就是需要新增一个交换机,但是不给这个交换机绑定队列,我来简单地在DirectRabitConfig里面新增一个直连交换机,名叫‘lonelyDirectExchange’,但没给它做任何绑定配置操作:
@Bean
DirectExchange lonelyDirectExchange() {
return new DirectExchange("lonelyDirectExchange");
}
然后写个测试接口,把消息推送到名为‘lonelyDirectExchange’的交换机上(这个交换机是没有任何队列配置的):
@GetMapping("/TestMessageAck2")
public String TestMessageAck2() {
String messageId = String.valueOf(UUID.randomUUID());
String messageData = "message: lonelyDirectExchange test message ";
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Map<String, Object> map = new HashMap<>();
map.put("messageId", messageId);
map.put("messageData", messageData);
map.put("createTime", createTime);
rabbitTemplate.convertAndSend("lonelyDirectExchange", "TestDirectRouting", map);
return "ok";
}
调用接口,查看rabbitmq-provuder项目的控制台输出情况:
ReturnCallback: 消息:(Body:'{createTime=2019-09-04 09:48:01, messageId=563077d9-0a77-4c27-8794-ecfb183eac80, messageData=message: lonelyDirectExchange test message }' MessageProperties [headers={}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])
ReturnCallback: 回应码:312
ReturnCallback: 回应信息:NO_ROUTE
ReturnCallback: 交换机:lonelyDirectExchange
ReturnCallback: 路由键:TestDirectRouting
ConfirmCallback: 相关数据:null
ConfirmCallback: 确认情况:true
ConfirmCallback: 原因:null
可以看到这种情况,两个函数都被调用了;这种情况下,消息是推送成功到服务器了的,所以ConfirmCallback对消息确认情况是true;而在RetrunCallback回调函数的打印参数里面可以看到,消息是推送到了交换机成功了,但是在路由分发给队列的时候,找不到队列,所以报了错误 NO_ROUTE 。
结论:②这种情况触发的是 ConfirmCallback和RetrunCallback两个回调函数。
③消息推送到sever,交换机和队列啥都没找到
这种情况其实一看就觉得跟①很像,没错 ,③和①情况回调是一致的,所以不做结果说明了。
结论: ③这种情况触发的是 ConfirmCallback 回调函数。
④消息推送成功
那么测试下,按照正常调用之前消息推送的接口就行,就调用下 /sendFanoutMessage接口,可以看到控制台输出:
ConfirmCallback: 相关数据:null
ConfirmCallback: 确认情况:true
ConfirmCallback: 原因:null
结论: ④这种情况触发的是 ConfirmCallback 回调函数。
消费者的消息应答模式,自动补偿(重试)机制
一、应答模式
和生产者的消息确认机制不同,因为消息接收本来就是在监听消息,符合条件的消息就会消费下来。所以,消息接收的确认机制主要存在三种模式:
①自动确认, 这也是默认的消息确认情况。 AcknowledgeMode.NONE
RabbitMQ成功将消息发出(即将消息成功写入TCP Socket)中立即认为本次投递已经被正确处理,不管消费者端是否成功处理本次投递。所以这种情况如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息。一般这种情况我们都是使用try catch捕捉异常后,打印日志用于追踪数据,这样找出对应数据再做后续处理。(建议:尽量不要使用手动ack,可能会导致消息的丢失
)
② 手动确认 , 这个比较关键,也是我们配置接收消息确认机制时,多数选择的模式。
消费者收到消息后,手动调用channel.basicAck()
/channel.basicNack()
/channel.basicReject()
后,RabbitMQ收到这些消息后,才认为本次投递成功。
channel.basicAck()
:用于肯定确认
channel.basicNack()
:用于否定确认(注意:这是AMQP 0-9-1的RabbitMQ扩展)
basic.basicReject()
:用于否定确认,但与basic.nack相比有一个限制:一次只能拒绝单条消息
消费者端以上的3个方法都表示消息已经被正确投递,但是channel.basicAck()表示消息已经被正确处理。而channel.basicNack(),channel.basicReject()表示没有被正确处理。
着重讲下reject,因为有时候一些场景是需要重新入列的。
- channel.basicReject(deliveryTag, true); 拒绝消费当前消息,如果第二参数传入true,就是将数据重新丢回队列里,那么下次还会消费这消息。设置false,就是告诉服务器,我已经知道这条消息数据了,因为一些原因拒绝它,而且服务器也把这个消息丢掉就行。 下次不想再消费这条消息了。
使用拒绝后重新入列这个确认模式要谨慎,因为一般都是出现异常的时候,catch异常再拒绝入列,选择是否重入列。但是如果使用不当会导致一些每次都被你重入列的消息一直消费-入列-消费-入列这样循环,会导致消息积压。
- 顺便也简单讲讲 nack,这个也是相当于设置不消费某条消息。
channel.basicNack(deliveryTag, false, true);
第一个参数依然是当前消息到的数据的唯一id;
第二个参数是指是否针对多条消息;如果是true,也就是说一次性针对当前通道的消息的tagID小于当前这条消息的,都拒绝确认。
第三个参数是指是否重新入列,也就是指不确认的消息是否重新丢回到队列里面去。
同样使用不确认后重新入列这个确认模式要谨慎,因为这里也可能因为考虑不周出现消息一直被重新丢回去的情况,导致积压。
二、自动补偿(重试)机制
问题产生背景
- 情况1:
消费者获取到消息后,调用第三方接口,但接口暂时无法访问,是否需要重试? 需要重试。 - 情况2:
消费者获取到消息后,抛出数据转换异常,是否需要重试? 不需要重试 。
总结:
对于情况2,如果消费者代码抛出异常是需要发布新版本才能解决的问题,那么不需要重试,重试也无济于事。应该采用日志记录+定时任务job健康检查+人工进行补偿。
代码示例
application.yml
server:
port: 9005
spring:
#配置rabbitMq 服务器
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest #登录用户名(默认的,也可以进行添加用户)
password: guest #登录密码(默认的,也可以进行修改)
#虚拟host 可以不设置,使用server默认host
virtual-host: /hkm
#消息确认配置项->>注意:如果使用AMQP模式的消息确认机制,则需要注释掉以下配置
#开启 confirm 确认机制,确认消息发送到交换机(Exchange),是否确定回调
publisher-confirm-type: correlated
#开启 return 确认机制,确认消息发送到队列(Queue),是否确定回调
publisher-returns: true
#设置为 true后消费者在消息没有被路由到合适队列情况下会被return监听,而不会自动删除
template:
mandatory: true
listener:
simple:
retry:
####开启消费者异常重试
enabled: true
####最大重试次数
max-attempts: 5
####重试间隔次数
initial-interval: 5000
####默认为自动应答,开启手动ack,只要cosumer没有手动ack,那么消息会一直存在消息队列中,只有手动ack了,才会将消息从消息队列中删除掉
acknowledge-mode: manual
default-requeue-rejected: false
boot:
admin:
notify:
mail:
#配置是否启用邮件通知 false是不起用
enabled: true
##方法一:增加超时时间(单位:ms)
monitor:
read-timeout: 30000
注意:需要开启重试机制
消息发送者
/**
* confirm消息确认机制:
* 根据交换机名称和路由键精确匹配到对应的消息队列
* @param exchangeName 交换机名称
*/
public void sendForNotScheduledForConfirm(String exchangeName){
JSONObject jsonObject = new JSONObject();
//TOPIC:一对多,即一个消息发送者可以被多个消息接收者监听
String userName = System.currentTimeMillis() + "";
try {
jsonObject.put("userName", userName);
jsonObject.put("toEmail", "2590392428@qq.com");
} catch (JSONException e) {
e.printStackTrace();
}
String msg = jsonObject.toString();
String messageId=UUID.randomUUID() + "";
Message message = MessageBuilder.withBody(msg.getBytes()).setContentType(MessageProperties.CONTENT_TYPE_JSON)
.setContentEncoding("utf-8").setMessageId(messageId).build();
// 构建回调返回的数据
CorrelationData correlationData = new CorrelationData(messageId);
//设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
rabbitTemplate.setMandatory(true);
//设置确认消息是否发送到交换机(Exchange)回调函数
rabbitTemplate.setConfirmCallback(rabbitConfirmAndReturn);
//设置确认消息是否发送到队列(Queue)回调函数
rabbitTemplate.setReturnCallback(rabbitConfirmAndReturn);
//PS:发送消息,该操作是异步的
rabbitTemplate.convertAndSend(exchangeName, DirectRabbitConfig.EMAIL_ROUTING_KEY, message,correlationData);
}
消息消费者
@RabbitHandler
@RabbitListener(queues = DirectRabbitConfig.EMAIL_QUEUE_NAME,containerFactory = "rabbitListenerContainerFactory")
public void processForAutoAckWithRetry(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
try{
// 获取全局MessageID
String messageId = message.getMessageProperties().getMessageId();
//获取投递的消息
String msg = new String(message.getBody(), "UTF-8");
PrintUtil.print(this.getClass(), "-->process()->>(direct交换机)邮件消费者获取生产者消息:\n messageId:" + messageId + " 投递的消息:" + msg);
JSONObject jsonObject = JsonUtil.jsonToPojo(msg, JSONObject.class);
String userName = jsonObject.getString("userName");
String toEmail = jsonObject.getString("toEmail");
// 请求地址
String emailUrl = "http://127.0.0.1:9004/email/sendEmail?toEmail=" + toEmail;
PrintUtil.print(this.getClass(),"->>processForAutoAck()->>邮件消费者开始调用第三方邮件服务器,emailUrl:" + emailUrl);
String result = HttpClientUtils.doGet(emailUrl);
// 如果调用第三方邮件接口无法访问,如何实现自动重试.
if (result == null) {
throw new Exception("调用第三方邮件服务器接口失败!,实现重试机制");
}
PrintUtil.print(this.getClass(),"->>processForAutoAck()->>邮件消费者结束调用第三方邮件服务器成功,result:" + result + "程序执行结束");
sendSimpleMail(toEmail, userName);
}catch (Exception e){
e.printStackTrace();
//PS:如果出现异常,手动抛出异常,从而实现重试机制。
throw new Exception("出现异常!!实现重试机制");
}
}
注意:如果在消费过程中出现异常,那么手动抛出异常,这样RabbitMQ消费者就会执行重试机制。
实现原理:RabbitMQ的重试机制原理:(最好启用手动ack)
1、@RabbitListener 底层 使用Aop进行拦截,如果程序没有抛出异常,自动提交事务。
2、如果Aop使用异常通知拦截 获取异常信息的话,自动实现补偿机制 ,该消息会缓存到rabbitmq服务器端进行存放,一直重试到不抛异常为准。
3、修改重试机制策略 一般默认情况下 间隔5秒重试一次。
MQ重试机制需要注意的问题:
1、MQ消费者幂等性问题如何解决:使用全局ID。
案例一:使用RabbitMQ模拟发送邮件,短信和接收邮件和短信的情景
架构图
代码示例
application.yml
server:
port: 9006
spring:
#配置rabbitMq 服务器
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest #登录用户名(默认的,也可以进行添加用户)
password: guest #登录密码(默认的,也可以进行修改)
#虚拟host 可以不设置,使用server默认host
virtual-host: /hkm
#消息确认配置项->>注意:如果使用AMQP模式的消息确认机制,则需要注释掉以下配置
#开启 confirm 确认机制,确认消息发送到交换机(Exchange),是否确定回调
publisher-confirm-type: correlated
#开启 return 确认机制,确认消息发送到队列(Queue),是否确定回调
publisher-returns: true
#设置为 true后消费者在消息没有被路由到合适队列情况下会被return监听,而不会自动删除
template:
mandatory: true
listener:
simple:
retry:
####开启消费者异常重试
enabled: true
####最大重试次数
max-attempts: 5
####重试间隔次数
initial-interval: 5000
####默认为自动应答,开启手动ack,只要cosumer没有手动ack,那么消息会一直存在消息队列中,只有手动ack了,才会将消息从消息队列中删除掉
acknowledge-mode: manual
default-requeue-rejected: false
boot:
admin:
notify:
mail:
#配置是否启用邮件通知 false是不起用
enabled: true
##方法一:增加超时时间(单位:ms)
monitor:
read-timeout: 30000
RabbitMQ配置
package com.sprjjs.rabbitmq.config;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.SerializerMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
@Configuration
public class RabbitMQConfig {
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
return factory;
}
@Bean
//注意:需要将RabbitTemplate 实例设置为prototype,因为一个RabbitTemplate 实例只能对应一个RabbitTemplate.ConfirmCallback实例,否则会报异常
@Scope("prototype")
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory);
//设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
rabbitTemplate.setMandatory(true);
rabbitTemplate.setMessageConverter(new SerializerMessageConverter());
return rabbitTemplate;
}
}
直连型交换机-队列配置
package com.sprjjs.rabbitmq.config.direct;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @Author : hkm
* @CreateTime : 2020/10/22
* @Description :direct交换机
**/
@Configuration
public class DirectRabbitConfig {
public static final String SMS_QUEUE_NAME = "direct_queue_sms";
public static final String SMS_ROUTING_KEY = "direct_routing_key_sms";
public static final String EMAIL_QUEUE_NAME = "direct_queue_email";
public static final String EMAIL_ROUTING_KEY = "direct_routing_key_email";
public static final String EXCHANGE_NAME = "direct_exchange";
/**
* 业务消息队列和死信交换机绑定的标识符
*/
public static final String DEAD_LETTER_EXCHANGE_KEY = "x-dead-letter-exchange";
/**
* 业务消息队列和死信交换机的绑定键的标识符
*/
public static final String DEAD_LETTER_ROUTING_KEY = "x-dead-letter-routing-key";
/**
* 定义优先级队列
*/
public static final String DEAD_LETTER_MAX_PRIORITY = "x-max-prioritye";
/**
* 设置消息发送到消息队列之后多久被丢弃,单位:毫秒
*/
public static final String DEAD_LETTER_MESSAGE_TTL = "x-message-ttl";
//短信消息队列
@Bean
public Queue directQueueSms() {
Map<String, Object> map = new HashMap<>();
/**
* 设置消息发送到消息队列之后多久被丢弃,单位:毫秒
*/
//map.put(DirectRabbitConfig.DEAD_LETTER_MESSAGE_TTL, DirectDLXRabbitConfig.DEAD_LETTER_MESSAGE_TTL);
/**
* key:业务消息队列和死信交换机绑定的标识符(DLX) value:死信交换机的名称
*/
map.put(DirectRabbitConfig.DEAD_LETTER_EXCHANGE_KEY, DirectDLXRabbitConfig.DIRECT_DLX_EXCHANGE_NAME);
/**
* key:业务消息队列和死信交换机的绑定键的标识符(DLK) value:业务消息队列和死信交换机的绑定键
*/
map.put(DirectRabbitConfig.DEAD_LETTER_ROUTING_KEY, DirectDLXRabbitConfig.DIRECT_SMS_DLX_ROUTING_KEY);
/**
* 定义优先级队列,消息最大优先级为15,优先级范围为0-15,数字越大优先级越高
*/
map.put(DirectRabbitConfig.DEAD_LETTER_MAX_PRIORITY, DirectDLXRabbitConfig.DEAD_LETTER_MAX_PRIORITY);
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,true:只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,默认也是false,这样会消息导致挤压,true:当没有生产者或者消费者使用此队列,该队列会自动删除。
/**
* 将业务消息队列和死信交换机进行绑定,并设置绑定键
*/
return new Queue(DirectRabbitConfig.SMS_QUEUE_NAME,true,false, false, map);
//return QueueBuilder.durable(DirectRabbitConfig.SMS_QUEUE_NAME).withArguments(map).build();
}
//邮件消息队列
@Bean
public Queue directQueueEmail() {
Map<String, Object> map = new HashMap<>();
/**
* 设置消息发送到消息队列之后多久被丢弃,单位:毫秒
*/
//map.put(DirectRabbitConfig.DEAD_LETTER_MESSAGE_TTL, DirectDLXRabbitConfig.DEAD_LETTER_MESSAGE_TTL);
/**
* key:业务消息队列和死信交换机绑定的标识符(DLX) value:死信交换机的名称
*/
map.put(DirectRabbitConfig.DEAD_LETTER_EXCHANGE_KEY, DirectDLXRabbitConfig.DIRECT_DLX_EXCHANGE_NAME);
/**
* key:业务消息队列和死信交换机的绑定键的标识符(DLK) value:业务消息队列和死信交换机的绑定键
*/
map.put(DirectRabbitConfig.DEAD_LETTER_ROUTING_KEY, DirectDLXRabbitConfig.DIRECT_EMAIL_DLX_ROUTING_KEY);
/**
* 定义优先级队列,消息最大优先级为15,优先级范围为0-15,数字越大优先级越高
*/
map.put(DirectRabbitConfig.DEAD_LETTER_MAX_PRIORITY, DirectDLXRabbitConfig.DEAD_LETTER_MAX_PRIORITY);
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,true:只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,默认也是false,这样会消息导致挤压,true:当没有生产者或者消费者使用此队列,该队列会自动删除。
/**
* 将业务消息队列和死信交换机进行绑定,并设置绑定键
*/
return new Queue(DirectRabbitConfig.EMAIL_QUEUE_NAME,true,false, false, map);
//return QueueBuilder.durable(DirectRabbitConfig.EMAIL_QUEUE_NAME).withArguments(map).build();
}
//direct交换机
@Bean
public DirectExchange directExchange() {
// return new DirectExchange("TestDirectExchange",true,true);
return new DirectExchange(DirectRabbitConfig.EXCHANGE_NAME,true,false);
}
//交换机,但是不绑定队列
@Bean
public DirectExchange directExchangeNotBindQueue() {
// return new DirectExchange("TestDirectExchange",true,true);
return new DirectExchange("notBindQueue",true,false);
}
//绑定 将短信消息队列和交换机绑定, 并设置绑定键
@Bean
public Binding bindingDirectWithSms() {
return BindingBuilder.bind(directQueueSms()).to(directExchange()).with(DirectRabbitConfig.SMS_ROUTING_KEY);
}
//绑定 将邮件消息队列和交换机绑定, 并设置绑定键
@Bean
public Binding bindingDirectWithEmail() {
return BindingBuilder.bind(directQueueEmail()).to(directExchange()).with(DirectRabbitConfig.EMAIL_ROUTING_KEY);
}
}
邮件消息生产者
package com.sprjjs.rabbitmq.producer.direct;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.rabbitmq.client.Channel;
import com.sprjjs.rabbitmq.utils.HttpClientUtils;
import com.sprjjs.rabbitmq.utils.PrintUtils;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.Connection;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.sprjjs.rabbitmq.config.RabbitConfirmAndReturn;
import com.sprjjs.rabbitmq.config.direct.DirectRabbitConfig;
import java.util.UUID;
/**
* 生产者发送消息出去之后,不知道到底有没有发送到RabbitMQ服务器,
* 默认是不知道的。而且有的时候我们在发送消息之后,后面的逻辑出问题了,我们不想要发送之前的消息了,需要撤回该怎么做。
* 解决方案:
* 1.AMQP 事务机制
* 2.Confirm 模式
* 事务模式:
* txSelect 将当前channel设置为transaction模式
* txCommit 提交当前事务
* txRollback 事务回滚
*/
@Component
public class ProducerEmailDirect {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RabbitConfirmAndReturn rabbitConfirmAndReturn;
@Autowired
private ConnectionFactory connectionFactory;
/**
* confirm消息确认机制:
* 根据交换机名称和路由键精确匹配到对应的消息队列
* @param exchangeName 交换机名称
*/
public void sendForNotScheduledForConfirm(String exchangeName){
JSONObject jsonObject = new JSONObject();
//TOPIC:一对多,即一个消息发送者可以被多个消息接收者监听
String userName = System.currentTimeMillis() + "";
try {
jsonObject.put("userName", userName);
jsonObject.put("toEmail", "2590392428@qq.com");
} catch (JSONException e) {
e.printStackTrace();
}
String msg = jsonObject.toString();
String messageId=UUID.randomUUID() + "";
Message message = MessageBuilder.withBody(msg.getBytes()).setContentType(MessageProperties.CONTENT_TYPE_JSON)
.setContentEncoding("utf-8").setMessageId(messageId).build();
// 构建回调返回的数据
CorrelationData correlationData = new CorrelationData(messageId);
//设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
rabbitTemplate.setMandatory(true);
//设置确认消息是否发送到交换机(Exchange)回调函数
rabbitTemplate.setConfirmCallback(rabbitConfirmAndReturn);
//设置确认消息是否发送到队列(Queue)回调函数
rabbitTemplate.setReturnCallback(rabbitConfirmAndReturn);
//PS:发送消息,该操作是异步的
rabbitTemplate.convertAndSend(exchangeName, DirectRabbitConfig.EMAIL_ROUTING_KEY, message,correlationData);
}
}
短信消息生产者
package com.sprjjs.rabbitmq.producer.direct;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.sprjjs.rabbitmq.config.RabbitConfirmAndReturn;
import com.sprjjs.rabbitmq.config.direct.DirectRabbitConfig;
import java.util.UUID;
@Component
public class ProducerSmsDirect {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RabbitConfirmAndReturn rabbitConfirmAndReturn;
@Autowired
private ConnectionFactory connectionFactory;
/**
* confirm消息确认机制:
* 根据交换机名称和路由键精确匹配到对应的消息队列
* @param exchangeName 交换机名称
*/
public void sendForNotScheduledForConfirm(String exchangeName){
JSONObject jsonObject = new JSONObject();
//TOPIC:一对多,即一个消息发送者可以被多个消息接收者监听
try {
jsonObject.put("toPhone", "13711769935");
} catch (JSONException e) {
e.printStackTrace();
}
String msg = jsonObject.toString();
String messageId=UUID.randomUUID() + "";
Message message = MessageBuilder.withBody(msg.getBytes()).setContentType(MessageProperties.CONTENT_TYPE_JSON)
.setContentEncoding("utf-8").setMessageId(messageId).build();
// 构建回调返回的数据
CorrelationData correlationData = new CorrelationData(messageId);
//设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
rabbitTemplate.setMandatory(true);
//设置确认消息是否发送到交换机(Exchange)回调函数
rabbitTemplate.setConfirmCallback(rabbitConfirmAndReturn);
//设置确认消息是否发送到队列(Queue)回调函数
rabbitTemplate.setReturnCallback(rabbitConfirmAndReturn);
//PS:发送消息,该操作是异步的
rabbitTemplate.convertAndSend(exchangeName, DirectRabbitConfig.SMS_ROUTING_KEY, message,correlationData);
}
}
邮件消息消费者
package com.sprjjs.rabbitmq.consumer.direct;
import com.alibaba.fastjson.JSONObject;
import com.rabbitmq.client.Channel;
import com.sprjjs.rabbitmq.utils.JsonUtils;
import com.sprjjs.rabbitmq.utils.PrintUtils;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import com.sprjjs.rabbitmq.config.direct.DirectRabbitConfig;
import java.util.Map;
@Component
public class ConsumerEmailDirect {
@Value("${spring.mail.username}")
private String fromEmail;
@Autowired
private JavaMailSender javaMailSender;
/**
* 死信队列被触发的情况:(最好使用手动ack)
* 1.消息被拒绝(basic.reject或basic.nack)并且requeue=false.
* 2.消息TTL过期
* 3.队列达到最大长度(队列满了,无法再添加数据到mq中)
* 死信队列被触发的执行流程:
* 生产者 --> 消息 --> 交换机 --> 队列 --> 变成死信 --> DLX交换机 -->队列 --> 消费者
*
*/
@RabbitHandler
@RabbitListener(queues = DirectRabbitConfig.EMAIL_QUEUE_NAME,containerFactory = "rabbitListenerContainerFactory")
public void processForManualAckForDLX(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
// 获取全局MessageID
String messageId = message.getMessageProperties().getMessageId();
//获取投递的消息
String msg = new String(message.getBody(), "UTF-8");
PrintUtils.print(this.getClass(), "-->processForManualAckForDLX()->>(direct交换机)邮件消费者获取生产者消息:\n messageId:" + messageId + " 投递的消息:" + msg);
JSONObject jsonObject = JsonUtils.jsonToPojo(msg, JSONObject.class);
String userName = jsonObject.getString("userName");
String toEmail = jsonObject.getString("toEmail");
Long deliveryTag=(Long) headers.get(AmqpHeaders.DELIVERY_TAG);
PrintUtils.print(this.getClass(), "-->processForManualAckForDLX()->>deliveryTag:"+ deliveryTag);
//Long deliveryTag2=message.getMessageProperties().getDeliveryTag();
//PrintUtils.print(this.getClass(), "-->processForManualAckForDLX()->>deliveryTag2:"+ deliveryTag2);
try {
int i = 1 / 0;
// 手动ack
// 手动签收,第二个参数,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
channel.basicAck(deliveryTag, true);
sendSimpleMail(toEmail, userName);
}catch (Exception e){
e.printStackTrace();
PrintUtils.print(this.getClass(),"->>processForManualAckForDLX()->>出现异常,丢弃该消息,交给DLX交换机进行消费....");
//PS:如果出现异常,丢弃该消息,从而消息会交给DLX交换机进行消费
/**
* 第一个参数依然是当前消息到的数据的唯一id;
* 第二个参数是指是否针对多条消息;如果是true,也就是说一次性针对当前通道的消息的tagID小于当前这条消息的,都拒绝确认。
* 第三个参数是指是否重新入列,也就是指不确认的消息是否重新丢回到队列里面去。
* 同样使用不确认后重新入列这个确认模式要谨慎,因为这里也可能因为考虑不周出现消息一直被重新丢回去的情况,导致积压。
* PS:特别注意:第三个参数必须设置为false,这样才会将信息从业务消息队列中移除掉,死信交换机才会进行触发,
* 从而接收并消费丢弃的消息;如果设置为true,那么消费者就会执行重试机制,一直不停的消费,
* 知道没有异常发生,才会消费成功,从而将信息从业务消息队列中移除掉。
*/
channel.basicNack(deliveryTag, true, false);
}
}
//发送邮件
public void sendSimpleMail(String toEmail, String userName) throws Exception {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(fromEmail);
message.setTo(toEmail);
message.setSubject("蚂蚁课堂|每特教育 新学员提醒");
message.setText("祝贺您,成为了我们" + userName + ",学员!");
javaMailSender.send(message);
PrintUtils.print(this.getClass(),"->>sendSimpleMail()->>(direct交换机)->>邮件发送完成," + JSONObject.toJSONString(message));
}
}
短信消息消费者
package com.sprjjs.rabbitmq.consumer.direct;
import com.alibaba.fastjson.JSONObject;
import com.rabbitmq.client.Channel;
import com.sprjjs.rabbitmq.utils.JsonUtils;
import com.sprjjs.rabbitmq.utils.PrintUtils;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import com.sprjjs.rabbitmq.config.direct.DirectRabbitConfig;
import java.util.Map;
import java.util.UUID;
@Component
public class ConsumerSMSDirect {
@Value("${spring.sms.phone}")
private String fromPhone;
/**
* 死信队列被触发的情况:(最好使用手动ack)
* 1.消息被拒绝(basic.reject或basic.nack)并且requeue=false.
* 2.消息TTL过期
* 3.队列达到最大长度(队列满了,无法再添加数据到mq中)
* 死信队列被触发的执行流程:
* 生产者 --> 消息 --> 交换机 --> 队列 --> 变成死信 --> DLX交换机 -->队列 --> 消费者
*
*/
@RabbitHandler
@RabbitListener(queues = DirectRabbitConfig.SMS_QUEUE_NAME,containerFactory = "rabbitListenerContainerFactory")
public void processForManualAckForDLX(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
// 获取全局MessageID
String messageId = message.getMessageProperties().getMessageId();
//获取投递的消息
String msg = new String(message.getBody(), "UTF-8");
PrintUtils.print(this.getClass(), "-->processForManualAckForDLX()->>(direct交换机)信息消费者获取生产者消息:\n messageId:" + messageId + " 投递的消息:" + msg);
JSONObject jsonObject = JsonUtils.jsonToPojo(msg, JSONObject.class);
String toPhone = jsonObject.getString("toPhone");
Long deliveryTag=(Long) headers.get(AmqpHeaders.DELIVERY_TAG);
PrintUtils.print(this.getClass(), "-->processForManualAckForDLX()->>deliveryTag:"+ deliveryTag);
//Long deliveryTag2=message.getMessageProperties().getDeliveryTag();
//PrintUtils.print(this.getClass(), "-->processForManualAckForDLX()->>deliveryTag2:"+ deliveryTag2);
try {
int i = 1 / 0;
// 手动ack
// 手动签收,第二个参数,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
channel.basicAck(deliveryTag, true);
sendValidation(toPhone);
}catch (Exception e){
e.printStackTrace();
PrintUtils.print(this.getClass(),"->>processForManualAckForDLX()->>出现异常,丢弃该消息,交给DLX交换机进行消费....");
//PS:如果出现异常,丢弃该消息,从而消息会交给DLX交换机进行消费
/**
* 第一个参数依然是当前消息到的数据的唯一id;
* 第二个参数是指是否针对多条消息;如果是true,也就是说一次性针对当前通道的消息的tagID小于当前这条消息的,都拒绝确认。
* 第三个参数是指是否重新入列,也就是指不确认的消息是否重新丢回到队列里面去。
* 同样使用不确认后重新入列这个确认模式要谨慎,因为这里也可能因为考虑不周出现消息一直被重新丢回去的情况,导致积压。
* PS:特别注意:第三个参数必须设置为false,这样才会将信息从业务消息队列中移除掉,死信交换机才会进行触发,
* 从而接收并消费丢弃的消息;如果设置为true,那么消费者就会执行重试机制,一直不停的消费,
* 知道没有异常发生,才会消费成功,从而将信息从业务消息队列中移除掉。
*/
channel.basicNack(deliveryTag, true, false);
}
}
//模拟发送验证码
public void sendValidation(String toPhone) throws Exception {
PrintUtils.print(this.getClass(),"->>sendValidation()->>(direct交换机)->>"+fromPhone+"发送给手机号码为:"+toPhone+"的验证码为:"+ UUID.randomUUID() + "");
}
}
测试
package com.sprjjs.rabbitmq.producer.test.direct;
import com.sprjjs.rabbitmq.RabbitMQProducer;
import com.sprjjs.rabbitmq.producer.direct.ProducerEmailDirect;
import com.sprjjs.rabbitmq.producer.direct.ProducerSmsDirect;
import com.sprjjs.rabbitmq.utils.SpringUtils;
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.SpringJUnit4ClassRunner;
import com.sprjjs.rabbitmq.config.direct.DirectRabbitConfig;
//spring整合junit
@RunWith(SpringJUnit4ClassRunner.class)
//当前类为springboot的测试类,并启动springboot
@SpringBootTest(classes = {RabbitMQProducer.class})
public class TestSendMessage {
@Autowired
private ProducerEmailDirect producerEmailDirect;
@Autowired
private ProducerSmsDirect producerSmsDirect;
//发送邮件:confirm消息确认机制:发送邮件,交换机和队列都存在
@Test
public void testProducerEmailDirectForConfirm() {
producerEmailDirect.sendForNotScheduledForConfirm(DirectRabbitConfig.EXCHANGE_NAME);
}
//发送短信:confirm消息确认机制:发送邮件,交换机和队列都存在
@Test
public void testProducerSmsDirectForConfirm() {
producerSmsDirect.sendForNotScheduledForConfirm(DirectRabbitConfig.EXCHANGE_NAME);
}
}
注:该案例使用的是direct交换机,由于篇幅过长的原因,其他的交换机,就不贴代码了,其实也就是大同小异,按葫芦画瓢的事情,小伙伴自己动脑摸索先呗,相信你们可以的!!!!
案例二:使用RabbitMQ模拟用户下单,外卖小哥抢单的情景。
首先先来分析下需求:当一个用户下单时,需要将订单发送到多个派单队列中,然后外卖小哥分别监听自己的派单队列,一有消息就到自己的队列中拿到订单消息,这是可能有些小伙伴就有疑惑了??一个订单只能被一个外卖小哥抢到呀,假如在fanout工作模式下,你这样不是每个外卖小哥都能拿得到同一个订单吗?
老铁们,不急,好吗,先卖个关子,后面在解答你们的疑惑。。。。
在解答之前,我们先来通过这篇博客了解下RabbitMQ 如何实现对同一个应用的多个节点进行广播,这有助于你们理解我这个项目的架构。
看完的小伙伴,应该猜得出来我这个项目的架构是如何的了吧,废话不多说,上图:
友情提示:图片有点小,建议下载好好理解下哈。。。。
对架构图了解的差不多了,就可以编写代码了,这里面用到了死信交换机,对死信交换机不太熟悉的,可以参考这篇博客哦,看完马上恍然大悟的感觉!!!我以你的人格担保。(偷笑。。。) 【RabbitMQ】一文带你搞定RabbitMQ死信队列.
项目搭建
数据库的设计
订单表:
PS:一定要设置订单编号为唯一约束,保证订单编号的唯一性。
外卖小哥表:
PS: 一定要将订单编号添加唯一约束,这样就可以保证一个订单编号只能被一个外面小哥抢到,从而上面小伙伴们疑惑的问题也就迎刃而解呀。。。。。
pom文件
<!-- 添加springboot对amqp的支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
application.yml
注意:
1、一定要将消息消费者的应答模式设置为手动模式;
2、消息生产者的消息确认机制设置为confirm模式;
3、开启开启消息消费者异常重试机制;
server:
port: 9006
spring:
#配置rabbitMq 服务器
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest #登录用户名(默认的,也可以进行添加用户)
password: guest #登录密码(默认的,也可以进行修改)
#虚拟host 可以不设置,使用server默认host
virtual-host: /hkm
#消息确认配置项->>注意:如果使用AMQP模式的消息确认机制,则需要注释掉以下配置
#开启 confirm 确认机制,确认消息发送到交换机(Exchange),是否确定回调
publisher-confirm-type: correlated
#开启 return 确认机制,确认消息发送到队列(Queue),是否确定回调
publisher-returns: true
#设置为 true后消费者在消息没有被路由到合适队列情况下会被return监听,而不会自动删除
template:
mandatory: true
listener:
simple:
retry:
####开启消费者异常重试
enabled: true
####最大重试次数
max-attempts: 5
####重试间隔次数
initial-interval: 5000
####默认为自动应答,开启手动ack,只要cosumer没有手动ack,那么消息会一直存在消息队列中,只有手动ack了,才会将消息从消息队列中删除掉
acknowledge-mode: manual
default-requeue-rejected: false
boot:
admin:
notify:
mail:
#配置是否启用邮件通知 false是不起用
enabled: true
##方法一:增加超时时间(单位:ms)
monitor:
read-timeout: 30000
交换机-队列配置
FanoutRabbitConfig业务交换机-队列类:
package com.sunnsoft.rabbitmq.order.config.fanout;
import com.sunnsoft.rabbitmq.order.config.dlx.direct.DirectDLXRabbitConfig;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @Author : hkm
* @CreateTime : 2020/10/22
* @Description :fanout交换机
**/
@Configuration
public class FanoutRabbitConfig {
//派单消息队列(张三)
public static final String SEND_ORDER_QUEUE_NAME_ZHANGSAN = "fanout_queue_send_order_zhangsan";
//派单消息队列(李四)
public static final String SEND_ORDER_QUEUE_NAME_LISI = "fanout_queue_send_order_lisi";
//派单消息队列(王老五)
public static final String SEND_ORDER_QUEUE_NAME_WLW = "fanout_queue_send_order_wlw";
//补单消息队列
public static final String REPL_ORDER_QUEUE_NAME = "fanout_queue_repl_order";
public static final String EXCHANGE_NAME = "fanout_order_exchange";
/**
* 业务消息队列和死信交换机绑定的标识符
*/
public static final String DEAD_LETTER_EXCHANGE_KEY = "x-dead-letter-exchange";
/**
* 业务消息队列和死信交换机的绑定键的标识符
*/
public static final String DEAD_LETTER_ROUTING_KEY = "x-dead-letter-routing-key";
/**
* 定义优先级队列
*/
public static final String DEAD_LETTER_MAX_PRIORITY = "x-max-prioritye";
/**
* 设置消息发送到消息队列之后多久被丢弃,单位:毫秒
*/
public static final String DEAD_LETTER_MESSAGE_TTL = "x-message-ttl";
//派单消息队列(张三)
@Bean
public Queue fanoutQueueSendOrderForZhangsan() {
Map<String, Object> map = new HashMap<>();
/**
* 设置消息发送到消息队列之后多久被丢弃,单位:毫秒
*/
//map.put(FanoutRabbitConfig.DEAD_LETTER_MESSAGE_TTL, DirectDLXRabbitConfig.DEAD_LETTER_MESSAGE_TTL);
/**
* key:业务消息队列和死信交换机绑定的标识符(DLX) value:死信交换机的名称
*/
map.put(FanoutRabbitConfig.DEAD_LETTER_EXCHANGE_KEY, DirectDLXRabbitConfig.DIRECT_ORDER_DLX_EXCHANGE_NAME);
/**
* key:业务消息队列和死信交换机的绑定键的标识符(DLK) value:业务消息队列和死信交换机的绑定键
*/
map.put(FanoutRabbitConfig.DEAD_LETTER_ROUTING_KEY, DirectDLXRabbitConfig.ZHANGSAN_DIRECT_SEND_ORDER_DLX_ROUTING_KEY);
/**
* 定义优先级队列,消息最大优先级为15,优先级范围为0-15,数字越大优先级越高
*/
map.put(FanoutRabbitConfig.DEAD_LETTER_MAX_PRIORITY, DirectDLXRabbitConfig.DEAD_LETTER_MAX_PRIORITY);
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,true:只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,默认也是false,这样会消息导致挤压,true:当没有生产者或者消费者使用此队列,该队列会自动删除。
return new Queue(FanoutRabbitConfig.SEND_ORDER_QUEUE_NAME_ZHANGSAN,true,false, false,map);
//return QueueBuilder.durable(FanoutRabbitConfig.SMS_QUEUE_NAME).withArguments(map).build();
}
//派单消息队列(李四)
@Bean
public Queue fanoutQueueSendOrderForLisi() {
Map<String, Object> map = new HashMap<>();
/**
* 设置消息发送到消息队列之后多久被丢弃,单位:毫秒
*/
//map.put(FanoutRabbitConfig.DEAD_LETTER_MESSAGE_TTL, DirectDLXRabbitConfig.DEAD_LETTER_MESSAGE_TTL);
/**
* key:业务消息队列和死信交换机绑定的标识符(DLX) value:死信交换机的名称
*/
map.put(FanoutRabbitConfig.DEAD_LETTER_EXCHANGE_KEY, DirectDLXRabbitConfig.DIRECT_ORDER_DLX_EXCHANGE_NAME);
/**
* key:业务消息队列和死信交换机的绑定键的标识符(DLK) value:业务消息队列和死信交换机的绑定键
*/
map.put(FanoutRabbitConfig.DEAD_LETTER_ROUTING_KEY, DirectDLXRabbitConfig.LISI_DIRECT_SEND_ORDER_DLX_ROUTING_KEY);
/**
* 定义优先级队列,消息最大优先级为15,优先级范围为0-15,数字越大优先级越高
*/
map.put(FanoutRabbitConfig.DEAD_LETTER_MAX_PRIORITY, DirectDLXRabbitConfig.DEAD_LETTER_MAX_PRIORITY);
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,true:只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,默认也是false,这样会消息导致挤压,true:当没有生产者或者消费者使用此队列,该队列会自动删除。
return new Queue(FanoutRabbitConfig.SEND_ORDER_QUEUE_NAME_LISI,true,false, false,map);
//return QueueBuilder.durable(FanoutRabbitConfig.SMS_QUEUE_NAME).withArguments(map).build();
}
//派单消息队列(王老五)
@Bean
public Queue fanoutQueueSendOrderForWlw() {
Map<String, Object> map = new HashMap<>();
/**
* 设置消息发送到消息队列之后多久被丢弃,单位:毫秒
*/
//map.put(FanoutRabbitConfig.DEAD_LETTER_MESSAGE_TTL, DirectDLXRabbitConfig.DEAD_LETTER_MESSAGE_TTL);
/**
* key:业务消息队列和死信交换机绑定的标识符(DLX) value:死信交换机的名称
*/
map.put(FanoutRabbitConfig.DEAD_LETTER_EXCHANGE_KEY, DirectDLXRabbitConfig.DIRECT_ORDER_DLX_EXCHANGE_NAME);
/**
* key:业务消息队列和死信交换机的绑定键的标识符(DLK) value:业务消息队列和死信交换机的绑定键
*/
map.put(FanoutRabbitConfig.DEAD_LETTER_ROUTING_KEY, DirectDLXRabbitConfig.WLW_DIRECT_SEND_ORDER_DLX_ROUTING_KEY);
/**
* 定义优先级队列,消息最大优先级为15,优先级范围为0-15,数字越大优先级越高
*/
map.put(FanoutRabbitConfig.DEAD_LETTER_MAX_PRIORITY, DirectDLXRabbitConfig.DEAD_LETTER_MAX_PRIORITY);
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,true:只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,默认也是false,这样会消息导致挤压,true:当没有生产者或者消费者使用此队列,该队列会自动删除。
return new Queue(FanoutRabbitConfig.SEND_ORDER_QUEUE_NAME_WLW,true,false, false,map);
//return QueueBuilder.durable(FanoutRabbitConfig.SMS_QUEUE_NAME).withArguments(map).build();
}
//补单消息队列
@Bean
public Queue fanoutQueueReplOrder() {
Map<String, Object> map = new HashMap<>();
/**
* 设置消息发送到消息队列之后多久被丢弃,单位:毫秒
*/
//map.put(FanoutRabbitConfig.DEAD_LETTER_MESSAGE_TTL, DirectDLXRabbitConfig.DEAD_LETTER_MESSAGE_TTL);
/**
* key:业务消息队列和死信交换机绑定的标识符(DLX) value:死信交换机的名称
*/
map.put(FanoutRabbitConfig.DEAD_LETTER_EXCHANGE_KEY, DirectDLXRabbitConfig.DIRECT_ORDER_DLX_EXCHANGE_NAME);
/**
* key:业务消息队列和死信交换机的绑定键的标识符(DLK) value:业务消息队列和死信交换机的绑定键
*/
map.put(FanoutRabbitConfig.DEAD_LETTER_ROUTING_KEY, DirectDLXRabbitConfig.DIRECT_REPL_ORDER_DLX_ROUTING_KEY);
/**
* 定义优先级队列,消息最大优先级为15,优先级范围为0-15,数字越大优先级越高
*/
map.put(FanoutRabbitConfig.DEAD_LETTER_MAX_PRIORITY, DirectDLXRabbitConfig.DEAD_LETTER_MAX_PRIORITY);
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,true:只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,默认也是false,这样会消息导致挤压,true:当没有生产者或者消费者使用此队列,该队列会自动删除。
return new Queue(FanoutRabbitConfig.REPL_ORDER_QUEUE_NAME,true,false, false,map);
//return QueueBuilder.durable(FanoutRabbitConfig.EMAIL_QUEUE_NAME).withArguments(map).build();
}
//fanout交换机
@Bean
public FanoutExchange fanoutExchange() {
// return new fanoutExchange("TestfanoutExchange",true,true);
return new FanoutExchange(FanoutRabbitConfig.EXCHANGE_NAME,true,false);
}
//绑定 将派单消息队列(张三)和交换机绑定, 不用设置绑定键,即使设置了也没用
@Bean
public Binding bindingFanoutSendOrderForZhangsan() {
return BindingBuilder.bind(fanoutQueueSendOrderForZhangsan()).to(fanoutExchange());
}
//绑定 将派单消息队列(李四)和交换机绑定, 不用设置绑定键,即使设置了也没用
@Bean
public Binding bindingFanoutSendOrderForLisi() {
return BindingBuilder.bind(fanoutQueueSendOrderForLisi()).to(fanoutExchange());
}
//绑定 将派单消息队列(王老五)和交换机绑定, 不用设置绑定键,即使设置了也没用
@Bean
public Binding bindingFanoutSendOrderForWlw() {
return BindingBuilder.bind(fanoutQueueSendOrderForWlw()).to(fanoutExchange());
}
//绑定 将补单消息队列和交换机绑定, 不用设置绑定键,即使设置了也没用
@Bean
public Binding bindingFanoutReplOrder() {
return BindingBuilder.bind(fanoutQueueReplOrder()).to(fanoutExchange());
}
}
DirectDLXRabbitConfig死信交换机-队列类:
package com.sunnsoft.rabbitmq.order.config.dlx.direct;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author : hkm
* @CreateTime : 2020/10/27
* @Description :死信交换机
**/
@Configuration
public class DirectDLXRabbitConfig {
//派单死信消息队列(张三)
public static final String ZHANGSAN_DIRECT_SEND_ORDER_DLX_QUEUE_NAME = "zhangsan_direct_send_order_queue_dlx";
public static final String ZHANGSAN_DIRECT_SEND_ORDER_DLX_ROUTING_KEY = "zhangsan_direct_send_order_routing_key_dlx";
//派单死信消息队列(李四)
public static final String LISI_DIRECT_SEND_ORDER_DLX_QUEUE_NAME = "lisi_direct_send_order_queue_dlx";
public static final String LISI_DIRECT_SEND_ORDER_DLX_ROUTING_KEY = "lisi_direct_send_order_routing_key_dlx";
//派单死信消息队列(王老五)
public static final String WLW_DIRECT_SEND_ORDER_DLX_QUEUE_NAME = "wlw_direct_send_order_queue_dlx";
public static final String WLW_DIRECT_SEND_ORDER_DLX_ROUTING_KEY = "wlw_direct_send_order_routing_key_dlx";
//补单死信消息队列
public static final String DIRECT_REPL_ORDER_DLX_QUEUE_NAME = "direct_repl_order_queue_dlx";
public static final String DIRECT_REPL_ORDER_DLX_ROUTING_KEY = "direct_repl_order_routing_key_dlx";
//死信交换机
public static final String DIRECT_ORDER_DLX_EXCHANGE_NAME = "direct_order_dlx_exchange";
/**
* 定义优先级队列
*/
public static final Long DEAD_LETTER_MAX_PRIORITY = 15L;
/**
* 设置消息发送到消息队列之后多久被丢弃,单位:毫秒
*/
public static final Long DEAD_LETTER_MESSAGE_TTL = 10000L;
//派单死信消息队列(张三)
@Bean
public Queue directQueueSendOrderDLXForZhangsan() {
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,true:只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,默认也是false,这样会消息导致挤压,true:当没有生产者或者消费者使用此队列,该队列会自动删除。
return new Queue(DirectDLXRabbitConfig.ZHANGSAN_DIRECT_SEND_ORDER_DLX_QUEUE_NAME,true,false, false);
//return QueueBuilder.durable(DLXRabbitConfig.DIRECT_EMAIL_DLX_QUEUE_NAME).build();
}
//派单死信消息队列(李四)
@Bean
public Queue directQueueSendOrderDLXForLisi() {
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,true:只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,默认也是false,这样会消息导致挤压,true:当没有生产者或者消费者使用此队列,该队列会自动删除。
return new Queue(DirectDLXRabbitConfig.LISI_DIRECT_SEND_ORDER_DLX_QUEUE_NAME,true,false, false);
//return QueueBuilder.durable(DLXRabbitConfig.DIRECT_EMAIL_DLX_QUEUE_NAME).build();
}
//派单死信消息队列(王老五)
@Bean
public Queue directQueueSendOrderDLXForWlw() {
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,true:只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,默认也是false,这样会消息导致挤压,true:当没有生产者或者消费者使用此队列,该队列会自动删除。
return new Queue(DirectDLXRabbitConfig.WLW_DIRECT_SEND_ORDER_DLX_QUEUE_NAME,true,false, false);
//return QueueBuilder.durable(DLXRabbitConfig.DIRECT_EMAIL_DLX_QUEUE_NAME).build();
}
//补单死信消息队列
@Bean
public Queue directQueueReplOrderDLX() {
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,true:只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,默认也是false,这样会消息导致挤压,true:当没有生产者或者消费者使用此队列,该队列会自动删除。
return new Queue(DirectDLXRabbitConfig.DIRECT_REPL_ORDER_DLX_QUEUE_NAME,true,false, false);
//return QueueBuilder.durable(DLXRabbitConfig.DIRECT_SMS_DLX_QUEUE_NAME).build();
}
//死信交换机
@Bean
public DirectExchange directExchangeDLX() {
// return new DirectExchange("TestDirectExchange",true,true);
return new DirectExchange(DirectDLXRabbitConfig.DIRECT_ORDER_DLX_EXCHANGE_NAME,true,false);
}
//绑定 将派单死信消息队列(张三)和死信交换机绑定, 并设置绑定键
@Bean
public Binding bindingFanoutDLXSendOrderForZhangsan() {
return BindingBuilder.bind(directQueueSendOrderDLXForZhangsan()).to(directExchangeDLX()).with(DirectDLXRabbitConfig.ZHANGSAN_DIRECT_SEND_ORDER_DLX_ROUTING_KEY);
}
//绑定 将派单死信消息队列(李四)和死信交换机绑定, 并设置绑定键
@Bean
public Binding bindingFanoutDLXSendOrderForLisi() {
return BindingBuilder.bind(directQueueSendOrderDLXForLisi()).to(directExchangeDLX()).with(DirectDLXRabbitConfig.LISI_DIRECT_SEND_ORDER_DLX_ROUTING_KEY);
}
//绑定 将派单死信消息队列(王老五)和死信交换机绑定, 并设置绑定键
@Bean
public Binding bindingFanoutDLXSendOrderForWlw() {
return BindingBuilder.bind(directQueueSendOrderDLXForWlw()).to(directExchangeDLX()).with(DirectDLXRabbitConfig.WLW_DIRECT_SEND_ORDER_DLX_ROUTING_KEY);
}
//绑定 将补单死信消息队列和死信交换机绑定, 并设置绑定键
@Bean
public Binding bindingFanoutDLXReplOrder() {
return BindingBuilder.bind(directQueueReplOrderDLX()).to(directExchangeDLX()).with(DirectDLXRabbitConfig.DIRECT_REPL_ORDER_DLX_ROUTING_KEY);
}
}
消息生产者(用户下单)
分析:用户下完单,就往补单和派单消息队列中发送订单编号。但此时可能会出现一种问题:就是用户下完单,将订单编号发送出去之后,后面就突然发生异常,这时事务回滚了,也就是该订单记录插入到订单表失败,但外卖小哥还是会拿到订单编号,这种情况肯定是不行的呀!!所以这时候就需要补单队列进行一个补单操作,从而保证订单表中的订单编号和外卖小哥表中的表单编号一致性。
package com.sunnsoft.rabbitmq.order.producer.fanout;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.sunnsoft.rabbitmq.order.commons.utils.GenerateOrderId;
import com.sunnsoft.rabbitmq.order.commons.utils.PrintUtils;
import com.sunnsoft.rabbitmq.order.config.RabbitConfirmAndReturn;
import com.sunnsoft.rabbitmq.order.data.model.entity.Torder;
import com.sunnsoft.rabbitmq.order.data.service.ITorderService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.UUID;
/**
* 生产者发送消息出去之后,不知道到底有没有发送到RabbitMQ服务器,
* 默认是不知道的。而且有的时候我们在发送消息之后,后面的逻辑出问题了,我们不想要发送之前的消息了,需要撤回该怎么做。
* 解决方案:
* 1.AMQP 事务机制
* 2.Confirm 模式
* 事务模式:
* txSelect 将当前channel设置为transaction模式
* txCommit 提交当前事务
* txRollback 事务回滚
*/
@Component
@Transactional(rollbackFor = Exception.class)
public class ProducerSendOrderFanout {
@Autowired
private RabbitTemplate rabbitTemplate;
@Resource
private ITorderService torderService;
@Autowired
private RabbitConfirmAndReturn rabbitConfirmAndReturn;
@Autowired
private ConnectionFactory connectionFactory;
/**
* 生产者:用户开始下单
* confirm消息确认机制:
* 根据交换机名称和路由键精确匹配到对应的消息队列
* @param exchangeName 交换机名称
*/
public void sendOrderForConfirm(String exchangeName) throws Exception{
PrintUtils.print(this.getClass(),"->>sendOrderForConfirm()->>有用户下单了,开始进行派单。。。。。");
JSONObject jsonObject = new JSONObject();
Long orderNum=Long.valueOf(GenerateOrderId.getRandomOrderId());
try {
jsonObject.put("orderNum", orderNum);
} catch (JSONException e) {
e.printStackTrace();
}
String msg = jsonObject.toString();
String messageId=UUID.randomUUID() + "";
Message message = MessageBuilder.withBody(msg.getBytes()).setContentType(MessageProperties.CONTENT_TYPE_JSON)
.setContentEncoding("utf-8").setMessageId(messageId).build();
// 构建回调返回的数据
CorrelationData correlationData = new CorrelationData(messageId);
//设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
rabbitTemplate.setMandatory(true);
//设置确认消息是否发送到交换机(Exchange)回调函数
rabbitTemplate.setConfirmCallback(rabbitConfirmAndReturn);
//设置确认消息是否发送到队列(Queue)回调函数
rabbitTemplate.setReturnCallback(rabbitConfirmAndReturn);
//PS:发送消息,该操作是异步的
rabbitTemplate.convertAndSend(exchangeName, "", message,correlationData);
Torder torder=new Torder();
torder.setOrderNum(orderNum);
/**
* PS:用户下单成功,往订单表中插入数据
*/
torderService.insert(torder);
/**
* PS:此时有种情况:消息已经发送出去成功了,但是此时出现异常,事务需要回滚。
* 所以会导致数据库中不存在该订单记录,但是消费者还是会继续消费该订单记录,
* 这样肯定是不可以的,所以需要利用一个补单队列继续补单操作。
*/
int i=1/0;
}
}
消息消费者(外卖小哥抢单或补单)
派单消费者
分析: 每个外卖小哥对应一个消息队列,这个时候会出现每个外卖小哥都可以拿到同一个订单编号,这与一个订单只能被一个外卖小哥抢到这个逻辑是不符的。
解决方案:所以需要将订单编号设置为唯一约束,这样就可以很简单的达到一个订单只能被一个外卖小哥抢到的效果,抢单成功,就手动应答消息;其他的外卖小哥抢单失败后,直接丢弃掉,从而将消息交给死信队列进行处理。
package com.sunnsoft.rabbitmq.order.consumer.fanout;
import com.alibaba.fastjson.JSONObject;
import com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException;
import com.rabbitmq.client.Channel;
import com.sunnsoft.rabbitmq.order.commons.utils.JsonUtils;
import com.sunnsoft.rabbitmq.order.commons.utils.PrintUtils;
import com.sunnsoft.rabbitmq.order.config.fanout.FanoutRabbitConfig;
import com.sunnsoft.rabbitmq.order.data.model.entity.TorderUser;
import com.sunnsoft.rabbitmq.order.data.service.ITorderUserService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import java.sql.SQLException;
import java.util.Map;
/**
* 死信队列被触发的情况:(最好使用手动ack)
* 1.消息被拒绝(basic.reject或basic.nack)并且requeue=false.
* 2.消息TTL过期
* 3.队列达到最大长度(队列满了,无法再添加数据到mq中)
* 死信队列被触发的执行流程:
* 生产者 --> 消息 --> 交换机 --> 队列 --> 变成死信 --> DLX交换机 -->队列 --> 消费者
*
*/
@Component
public class ConsumerSendOrderFanout {
@Autowired
private ITorderUserService torderUserService;
/**
* 死信队列被触发的情况:(最好使用手动ack)
* 1.消息被拒绝(basic.reject或basic.nack)并且requeue=false.
* 2.消息TTL过期
* 3.队列达到最大长度(队列满了,无法再添加数据到mq中)
* 死信队列被触发的执行流程:
* 生产者 --> 消息 --> 交换机 --> 队列 --> 变成死信 --> DLX交换机 -->队列 --> 消费者
*
*/
//派单消息队列:外卖小哥张三
@RabbitHandler
@RabbitListener(queues = FanoutRabbitConfig.SEND_ORDER_QUEUE_NAME_ZHANGSAN,containerFactory = "rabbitListenerContainerFactory")
public void processForManualAckZhangsan(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
// 获取全局MessageID
String messageId = message.getMessageProperties().getMessageId();
//获取投递的消息
String msg = new String(message.getBody(), "UTF-8");
JSONObject jsonObject = JsonUtils.jsonToPojo(msg, JSONObject.class);
Long orderNum = Long.valueOf(jsonObject.getString("orderNum"));
Long deliveryTag=(Long) headers.get(AmqpHeaders.DELIVERY_TAG);
PrintUtils.print(this.getClass(), "-->processForManualAckZhangsan()->>(张三派单交换机)deliveryTag:"+ deliveryTag+"\nmessageId:" + messageId);
//Long deliveryTag2=message.getMessageProperties().getDeliveryTag();
//PrintUtils.print(this.getClass(), "-->processForManualAckForDLX()->>deliveryTag2:"+ deliveryTag2);
try {
/**
* PS:外卖小哥接到单后,直接插入一条数据
* 如果插入失败,那么说明已经被别的外卖小哥接了,那么直接放弃消费。
* (因为数据库中的订单编号设置为唯一约束,从而保证一个订单只被一个外卖小哥接收)
*
*/
//int i = 1 / 0;
TorderUser torderUser=new TorderUser();
torderUser.setOrderNum(orderNum);
torderUser.setUserId(1L);
torderUserService.insert(torderUser);
// 手动签收,第二个参数,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
channel.basicAck(deliveryTag, true);
PrintUtils.print(this.getClass(),"->>processForManualAckZhangsan()->>外卖小哥张三接单成功,订单编号"+orderNum);
}catch (DuplicateKeyException e){
e.printStackTrace();
PrintUtils.print(this.getClass(),"->>processForManualAckZhangsan()->>(张三派单交换机)外卖小哥张三接单失败,丢弃该消息,订单"+orderNum+"已被别的外卖小哥抢走");
}catch (Exception e){
e.printStackTrace();
PrintUtils.print(this.getClass(),"->>processForManualAckZhangsan()->>(张三派单交换机)出现异常,丢弃该消息,交给DLX交换机进行消费...");
}
//PS:如果出现异常,丢弃该消息,从而消息会交给DLX交换机进行消费
/**
* 第一个参数依然是当前消息到的数据的唯一id;
* 第二个参数是指是否针对多条消息;如果是true,也就是说一次性针对当前通道的消息的tagID小于当前这条消息的,都拒绝确认。
* 第三个参数是指是否重新入列,也就是指不确认的消息是否重新丢回到队列里面去。
* 同样使用不确认后重新入列这个确认模式要谨慎,因为这里也可能因为考虑不周出现消息一直被重新丢回去的情况,导致积压。
* PS:特别注意:第三个参数必须设置为false,这样才会将信息从业务消息队列中移除掉,死信交换机才会进行触发,
* 从而接收并消费丢弃的消息;如果设置为true,那么消费者就会执行重试机制,一直不停的消费,
* 知道没有异常发生,才会消费成功,从而将信息从业务消息队列中移除掉。
*/
channel.basicNack(deliveryTag, true, false);
}
/**
* 死信队列被触发的情况:(最好使用手动ack)
* 1.消息被拒绝(basic.reject或basic.nack)并且requeue=false.
* 2.消息TTL过期
* 3.队列达到最大长度(队列满了,无法再添加数据到mq中)
* 死信队列被触发的执行流程:
* 生产者 --> 消息 --> 交换机 --> 队列 --> 变成死信 --> DLX交换机 -->队列 --> 消费者
*
*/
//派单消息队列:外卖小哥李四
@RabbitHandler
@RabbitListener(queues = FanoutRabbitConfig.SEND_ORDER_QUEUE_NAME_LISI,containerFactory = "rabbitListenerContainerFactory")
public void processForManualAckLisi(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
// 获取全局MessageID
String messageId = message.getMessageProperties().getMessageId();
//获取投递的消息
String msg = new String(message.getBody(), "UTF-8");
JSONObject jsonObject = JsonUtils.jsonToPojo(msg, JSONObject.class);
Long orderNum = Long.valueOf(jsonObject.getString("orderNum"));
Long deliveryTag=(Long) headers.get(AmqpHeaders.DELIVERY_TAG);
PrintUtils.print(this.getClass(), "-->processForManualAckLisi()->>(李四派单交换机)deliveryTag:"+ deliveryTag+"\nmessageId:" + messageId);
//Long deliveryTag2=message.getMessageProperties().getDeliveryTag();
//PrintUtils.print(this.getClass(), "-->processForManualAckForDLX()->>deliveryTag2:"+ deliveryTag2);
try {
/**
* PS:外卖小哥接到单后,直接插入一条数据
* 如果插入失败,那么说明已经被别的外卖小哥接了,那么直接放弃消费。
* (因为数据库中的订单编号设置为唯一约束,从而保证一个订单只被一个外卖小哥接收)
*
*/
//int i = 1 / 0;
TorderUser torderUser=new TorderUser();
torderUser.setOrderNum(orderNum);
torderUser.setUserId(2L);
torderUserService.insert(torderUser);
// 手动签收,第二个参数,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
channel.basicAck(deliveryTag, true);
PrintUtils.print(this.getClass(),"->>processForManualAckLisi()->>外卖小哥李四接单成功,订单"+orderNum);
}catch (DuplicateKeyException e){
e.printStackTrace();
PrintUtils.print(this.getClass(),"->>processForManualAckLisi()->>(李四派单交换机)外卖小哥李四接单失败,丢弃该消息,订单"+orderNum+"已被别的外卖小哥抢走");
}catch (Exception e){
e.printStackTrace();
PrintUtils.print(this.getClass(),"->>processForManualAckLisi()->>(李四派单交换机)出现异常,丢弃该消息,交给DLX交换机进行消费...");
}
//PS:如果出现异常,丢弃该消息,从而消息会交给DLX交换机进行消费
/**
* 第一个参数依然是当前消息到的数据的唯一id;
* 第二个参数是指是否针对多条消息;如果是true,也就是说一次性针对当前通道的消息的tagID小于当前这条消息的,都拒绝确认。
* 第三个参数是指是否重新入列,也就是指不确认的消息是否重新丢回到队列里面去。
* 同样使用不确认后重新入列这个确认模式要谨慎,因为这里也可能因为考虑不周出现消息一直被重新丢回去的情况,导致积压。
* PS:特别注意:第三个参数必须设置为false,这样才会将信息从业务消息队列中移除掉,死信交换机才会进行触发,
* 从而接收并消费丢弃的消息;如果设置为true,那么消费者就会执行重试机制,一直不停的消费,
* 知道没有异常发生,才会消费成功,从而将信息从业务消息队列中移除掉。
*/
channel.basicNack(deliveryTag, true, false);
}
/**
* 死信队列被触发的情况:(最好使用手动ack)
* 1.消息被拒绝(basic.reject或basic.nack)并且requeue=false.
* 2.消息TTL过期
* 3.队列达到最大长度(队列满了,无法再添加数据到mq中)
* 死信队列被触发的执行流程:
* 生产者 --> 消息 --> 交换机 --> 队列 --> 变成死信 --> DLX交换机 -->队列 --> 消费者
*
*/
//派单消息队列:外卖小哥王老五
@RabbitHandler
@RabbitListener(queues = FanoutRabbitConfig.SEND_ORDER_QUEUE_NAME_WLW,containerFactory = "rabbitListenerContainerFactory")
public void processForManualAckWlw(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
// 获取全局MessageID
String messageId = message.getMessageProperties().getMessageId();
//获取投递的消息
String msg = new String(message.getBody(), "UTF-8");
JSONObject jsonObject = JsonUtils.jsonToPojo(msg, JSONObject.class);
Long orderNum = Long.valueOf(jsonObject.getString("orderNum"));
Long deliveryTag=(Long) headers.get(AmqpHeaders.DELIVERY_TAG);
PrintUtils.print(this.getClass(), "-->processForManualAckWlw()->>(王老五派单交换机)deliveryTag:"+ deliveryTag+"\nmessageId:" + messageId);
//Long deliveryTag2=message.getMessageProperties().getDeliveryTag();
//PrintUtils.print(this.getClass(), "-->processForManualAckForDLX()->>deliveryTag2:"+ deliveryTag2);
try {
/**
* PS:外卖小哥接到单后,直接插入一条数据
* 如果插入失败,那么说明已经被别的外卖小哥接了,那么直接放弃消费。
* (因为数据库中的订单编号设置为唯一约束,从而保证一个订单只被一个外卖小哥接收)
*
*/
//int i = 1 / 0;
TorderUser torderUser=new TorderUser();
torderUser.setOrderNum(orderNum);
torderUser.setUserId(3L);
torderUserService.insert(torderUser);
// 手动签收,第二个参数,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
channel.basicAck(deliveryTag, true);
PrintUtils.print(this.getClass(),"->>processForManualAckWlw()->>外卖小哥王老五接单成功,订单"+orderNum);
}catch (DuplicateKeyException e){
e.printStackTrace();
PrintUtils.print(this.getClass(),"->>processForManualAckWlw()->>(王老五派单交换机)外卖小哥王老五接单失败,丢弃该消息,订单"+orderNum+"已被别的外卖小哥抢走");
}catch (Exception e){
e.printStackTrace();
PrintUtils.print(this.getClass(),"->>processForManualAckWlw()->>(王老五派单交换机)出现异常,丢弃该消息,交给DLX交换机进行消费...");
}
//PS:如果出现异常,丢弃该消息,从而消息会交给DLX交换机进行消费
/**
* 第一个参数依然是当前消息到的数据的唯一id;
* 第二个参数是指是否针对多条消息;如果是true,也就是说一次性针对当前通道的消息的tagID小于当前这条消息的,都拒绝确认。
* 第三个参数是指是否重新入列,也就是指不确认的消息是否重新丢回到队列里面去。
* 同样使用不确认后重新入列这个确认模式要谨慎,因为这里也可能因为考虑不周出现消息一直被重新丢回去的情况,导致积压。
* PS:特别注意:第三个参数必须设置为false,这样才会将信息从业务消息队列中移除掉,死信交换机才会进行触发,
* 从而接收并消费丢弃的消息;如果设置为true,那么消费者就会执行重试机制,一直不停的消费,
* 知道没有异常发生,才会消费成功,从而将信息从业务消息队列中移除掉。
*/
channel.basicNack(deliveryTag, true, false);
}
}
派单死信消费者
分析: 上面已经分析了这样一种情况:如果其他的外卖小哥抢单失败后;或者在抢单过程中出现逻辑异常, 会直接丢弃掉消息,从而将消息交给派单死信队列进行处理,防止消息的丢失。
package com.sunnsoft.rabbitmq.order.consumer.dlx.direct;
import com.alibaba.fastjson.JSONObject;
import com.rabbitmq.client.Channel;
import com.sunnsoft.rabbitmq.order.commons.utils.JsonUtils;
import com.sunnsoft.rabbitmq.order.commons.utils.PrintUtils;
import com.sunnsoft.rabbitmq.order.config.dlx.direct.DirectDLXRabbitConfig;
import com.sunnsoft.rabbitmq.order.data.model.entity.TorderUser;
import com.sunnsoft.rabbitmq.order.data.service.ITorderUserService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class ComsumerDLXSendOrderDirect {
@Autowired
private ITorderUserService torderUserService;
/**
* 派单死信消息队列(张三)
* @param message
* @param headers
* @param channel
* @throws Exception
*/
@RabbitHandler
@RabbitListener(queues = DirectDLXRabbitConfig.ZHANGSAN_DIRECT_SEND_ORDER_DLX_QUEUE_NAME,containerFactory = "rabbitListenerContainerFactory")
public void processForManualAckDLXForZhangsan(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
// 获取全局MessageID
String messageId = message.getMessageProperties().getMessageId();
//获取投递的消息
String msg = new String(message.getBody(), "UTF-8");
JSONObject jsonObject = JsonUtils.jsonToPojo(msg, JSONObject.class);
Long orderNum = Long.valueOf(jsonObject.getString("orderNum"));
PrintUtils.print(this.getClass(),"->>processForManualAckDLXForZhangsan()->>(张三派单死信交换机)开始进行消费....获取生产者消息:\n messageId:" + messageId + " 订单编号:" + orderNum);
try {
// 手动ack
TorderUser torderUser=new TorderUser();
torderUser.setOrderNum(orderNum);
torderUser.setUserId(1L);
torderUserService.insert(torderUser);
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
//Long deliveryTag2=message.getMessageProperties().getDeliveryTag();
//PrintUtils.print(this.getClass(), "-->processForManualAckForDLX()->>deliveryTag2:"+ deliveryTag2);
// 手动签收,第二个参数,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
channel.basicAck(deliveryTag, true);
PrintUtils.print(this.getClass(),"->>processForManualAckDLXForZhangsan()->>(张三派单死信交换机)派单成功。。。。");
}catch (Exception e){
e.printStackTrace();
PrintUtils.print(this.getClass(),"->>processForManualAckDLXForZhangsan()->>(张三派单死信交换机)出现异常,派单失败,订单"+orderNum+"已被别的外卖小哥抢走。。。。");
}
}
/**
* 派单死信消息队列(李四)
* @param message
* @param headers
* @param channel
* @throws Exception
*/
@RabbitHandler
@RabbitListener(queues = DirectDLXRabbitConfig.LISI_DIRECT_SEND_ORDER_DLX_QUEUE_NAME,containerFactory = "rabbitListenerContainerFactory")
public void processForManualAckDLXForLisi(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
// 获取全局MessageID
String messageId = message.getMessageProperties().getMessageId();
//获取投递的消息
String msg = new String(message.getBody(), "UTF-8");
JSONObject jsonObject = JsonUtils.jsonToPojo(msg, JSONObject.class);
Long orderNum = Long.valueOf(jsonObject.getString("orderNum"));
PrintUtils.print(this.getClass(),"->>processForManualAckDLXForLisi()->>(李四派单死信交换机)开始进行消费....获取生产者消息:\n messageId:" + messageId + " 订单编号:" + orderNum);
try {
// 手动ack
TorderUser torderUser=new TorderUser();
torderUser.setOrderNum(orderNum);
torderUser.setUserId(2L);
torderUserService.insert(torderUser);
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
//Long deliveryTag2=message.getMessageProperties().getDeliveryTag();
//PrintUtils.print(this.getClass(), "-->processForManualAckForDLX()->>deliveryTag2:"+ deliveryTag2);
// 手动签收,第二个参数,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
channel.basicAck(deliveryTag, true);
PrintUtils.print(this.getClass(),"->>processForManualAckDLXForLisi()->>(李四派单死信交换机)派单成功。。。。");
}catch (Exception e){
e.printStackTrace();
PrintUtils.print(this.getClass(),"->>processForManualAckDLXForLisi()->>(李四派单死信交换机)出现异常,派单失败,订单"+orderNum+"已被别的外卖小哥抢走。。。");
}
}
/**
* 派单死信消息队列(王老五)
* @param message
* @param headers
* @param channel
* @throws Exception
*/
@RabbitHandler
@RabbitListener(queues = DirectDLXRabbitConfig.WLW_DIRECT_SEND_ORDER_DLX_QUEUE_NAME,containerFactory = "rabbitListenerContainerFactory")
public void processForManualAckDLXForWlw(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
// 获取全局MessageID
String messageId = message.getMessageProperties().getMessageId();
//获取投递的消息
String msg = new String(message.getBody(), "UTF-8");
JSONObject jsonObject = JsonUtils.jsonToPojo(msg, JSONObject.class);
Long orderNum = Long.valueOf(jsonObject.getString("orderNum"));
PrintUtils.print(this.getClass(),"->>processForManualAckDLXForWlw()->>(王老五派单死信交换机)开始进行消费....获取生产者消息:\n messageId:" + messageId + " 订单编号:" + orderNum);
try {
// 手动ack
TorderUser torderUser=new TorderUser();
torderUser.setOrderNum(orderNum);
torderUser.setUserId(3L);
torderUserService.insert(torderUser);
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
//Long deliveryTag2=message.getMessageProperties().getDeliveryTag();
//PrintUtils.print(this.getClass(), "-->processForManualAckForDLX()->>deliveryTag2:"+ deliveryTag2);
// 手动签收,第二个参数,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
channel.basicAck(deliveryTag, true);
PrintUtils.print(this.getClass(),"->>processForManualAckDLXForWlw()->>(王老五派单死信交换机)派单成功。。。。");
}catch (Exception e){
e.printStackTrace();
PrintUtils.print(this.getClass(),"->>processForManualAckDLXForWlw()->>(王老五派单死信交换机)出现异常,派单失败,订单"+orderNum+"已被别的外卖小哥抢走。。。");
}
}
}
补单消费者
分析:上面在用户下单的时候也分析过这么个问题:就是用户下完单,将订单编号发送出去之后,后面就突然发生异常,这时事务回滚了,也就是该订单记录插入到订单表失败,但外卖小哥还是会拿到订单,这种情况肯定是不行的呀!!
所以解决发案就是:添加一个补单队列进行一个补单操作,从而保证订单表中的订单编号和外卖小哥表中的表单编号一致性。
具体怎么补单呢??也就是根据接收的订单编号查询订单表,如果有该条订单记录,那么怒需要补单,否则,就进行补单。
是不是so easy,这个思想。。。。。
package com.sunnsoft.rabbitmq.order.consumer.fanout;
import com.alibaba.fastjson.JSONObject;
import com.rabbitmq.client.Channel;
import com.sunnsoft.rabbitmq.order.commons.utils.JsonUtils;
import com.sunnsoft.rabbitmq.order.commons.utils.PrintUtils;
import com.sunnsoft.rabbitmq.order.config.fanout.FanoutRabbitConfig;
import com.sunnsoft.rabbitmq.order.data.model.entity.Torder;
import com.sunnsoft.rabbitmq.order.data.service.ITorderService;
import org.apache.tomcat.util.security.PrivilegedGetTccl;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.UUID;
@Component
public class ConsumerReplOrderFanout {
@Autowired
private ITorderService torderService;
/**
* 死信队列被触发的情况:(最好使用手动ack)
* 1.消息被拒绝(basic.reject或basic.nack)并且requeue=false.
* 2.消息TTL过期
* 3.队列达到最大长度(队列满了,无法再添加数据到mq中)
* 死信队列被触发的执行流程:
* 生产者 --> 消息 --> 交换机 --> 队列 --> 变成死信 --> DLX交换机 -->队列 --> 消费者
*
*/
//补单消息队列
@RabbitHandler
@RabbitListener(queues = FanoutRabbitConfig.REPL_ORDER_QUEUE_NAME,containerFactory = "rabbitListenerContainerFactory")
public void processForManualAck(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
// 获取全局MessageID
String messageId = message.getMessageProperties().getMessageId();
//获取投递的消息
String msg = new String(message.getBody(), "UTF-8");
JSONObject jsonObject = JsonUtils.jsonToPojo(msg, JSONObject.class);
Long orderNum = Long.valueOf(jsonObject.getString("orderNum"));
PrintUtils.print(this.getClass(), "-->processForManualAck()->>(补单交换机)获取生产者消息:\n messageId:" + messageId + " 订单编号:" + orderNum);
Long deliveryTag=(Long) headers.get(AmqpHeaders.DELIVERY_TAG);
PrintUtils.print(this.getClass(), "-->processForManualAck()->>deliveryTag:"+ deliveryTag);
//Long deliveryTag2=message.getMessageProperties().getDeliveryTag();
//PrintUtils.print(this.getClass(), "-->processForManualAckForDLX()->>deliveryTag2:"+ deliveryTag2);
try {
/**
* 补单机制:先根据订单编号查询是否存在订单信息,如果没有,则进行补单机制,否则不补单
*/
Torder torder = torderService.selectByOrderId(orderNum);
if(torder==null){
PrintUtils.print(this.getClass(), "-->processForManualAck()->>(补单交换机)订单不存在,进行补单");
torder=new Torder();
torder.setOrderNum(orderNum);
/**
* PS:用户下单成功,往订单表中插入数据
*/
torderService.insert(torder);
PrintUtils.print(this.getClass(), "-->processForManualAck()->>(补单交换机)补单成功,补单的订单编号为:"+orderNum);
}else{
PrintUtils.print(this.getClass(), "-->processForManualAck()->>(补单交换机)不用补单,订单已存在");
}
//int i = 1 / 0;
// 手动ack
// 手动签收,第二个参数,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
channel.basicAck(deliveryTag, true);
}catch (Exception e){
e.printStackTrace();
PrintUtils.print(this.getClass(),"->>processForManualAck()->>(补单交换机)出现异常,补单失败,丢弃该消息,交给DLX交换机进行补单....");
//PS:如果出现异常,丢弃该消息,从而消息会交给DLX交换机进行消费
/**
* 第一个参数依然是当前消息到的数据的唯一id;
* 第二个参数是指是否针对多条消息;如果是true,也就是说一次性针对当前通道的消息的tagID小于当前这条消息的,都拒绝确认。
* 第三个参数是指是否重新入列,也就是指不确认的消息是否重新丢回到队列里面去。
* 同样使用不确认后重新入列这个确认模式要谨慎,因为这里也可能因为考虑不周出现消息一直被重新丢回去的情况,导致积压。
* PS:特别注意:第三个参数必须设置为false,这样才会将信息从业务消息队列中移除掉,死信交换机才会进行触发,
* 从而接收并消费丢弃的消息;如果设置为true,那么消费者就会执行重试机制,一直不停的消费,
* 知道没有异常发生,才会消费成功,从而将信息从业务消息队列中移除掉。
*/
channel.basicNack(deliveryTag, true, false);
}
}
}
补单死信消费者
分析:但补单过程中如果出现异常,那么会直接将消息丢弃掉,交给补单死信队列处理,防止数据的丢失。
package com.sunnsoft.rabbitmq.order.consumer.dlx.direct;
import com.alibaba.fastjson.JSONObject;
import com.rabbitmq.client.Channel;
import com.sunnsoft.rabbitmq.order.commons.utils.JsonUtils;
import com.sunnsoft.rabbitmq.order.commons.utils.PrintUtils;
import com.sunnsoft.rabbitmq.order.config.dlx.direct.DirectDLXRabbitConfig;
import com.sunnsoft.rabbitmq.order.data.model.entity.Torder;
import com.sunnsoft.rabbitmq.order.data.service.ITorderService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.UUID;
@Component
public class ComsumerDLXReplOrderDirect {
@Autowired
private ITorderService torderService;
/**
* 补单死信消息队列
* @param message
* @param headers
* @param channel
* @throws Exception
*/
@RabbitHandler
@RabbitListener(queues = DirectDLXRabbitConfig.DIRECT_REPL_ORDER_DLX_QUEUE_NAME,containerFactory = "rabbitListenerContainerFactory")
public void processForManualAckDLXForReplOrder(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
try {
// 获取全局MessageID
String messageId = message.getMessageProperties().getMessageId();
//获取投递的消息
String msg = new String(message.getBody(), "UTF-8");
JSONObject jsonObject = JsonUtils.jsonToPojo(msg, JSONObject.class);
Long orderNum = Long.valueOf(jsonObject.getString("orderNum"));
Torder torder=new Torder();
torder.setOrderNum(orderNum);
PrintUtils.print(this.getClass(),"->>processForManualAckDLXForReplOrder()->>(补单死信交换机)开始进行补单....获取生产者消息:\n messageId:" + messageId + " 补单订单编号:" + orderNum);
torderService.insert(torder);
// 手动ack
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
//Long deliveryTag2=message.getMessageProperties().getDeliveryTag();
//PrintUtils.print(this.getClass(), "-->processForManualAckForDLX()->>deliveryTag2:"+ deliveryTag2);
// 手动签收,第二个参数,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
PrintUtils.print(this.getClass(),"->>processForManualAckDLXForReplOrder()->>(补单死信交换机)补单成功。。。。");
channel.basicAck(deliveryTag, true);
}catch (Exception e){
e.printStackTrace();
PrintUtils.print(this.getClass(),"->>processForManualAckDLXForReplOrder()->>(补单死信交换机)出现异常,补单失败。。。。");
}
}
}
感言:哎。。。。先长叹一口气先,为什么要叹气呢,一是这篇博客花了我4天时间写的,很是用心了哈;二是这是我的第一篇博客,写完竟然有很强的成就感,哈哈哈。。。能认认真真看完上面,从而读到我的感言的小伙伴们,那肯定是。。。。。。我感动的泪水都要下来了。。。。。
总而言之:知易行难,文章的结束,正是行动的开始,愿你用行动,给自己创造一片繁花似锦。
如果本文对你有帮助,记得点个赞,也希望能分享给更多的朋友哦,么么哒。。。。
友情提示:案例二的源码我已上传到我的github:https://github.com/hkmhso/rabbitmq-order
感谢博客RabbitMQ 如何实现对同一个应用的多个节点进行广播 和【RabbitMQ】一文带你搞定RabbitMQ死信队列的支持。
本文由昵称:某一个有b格的程序yuan博主原创,转载请注明来源:一篇带你立马搞定消息中间件,并使用RabbitMQ模拟用户下单,外卖小哥抢单的情景。,谢谢~~~