一篇带你立马搞定消息中间件,并使用RabbitMQ模拟用户下单,外卖小哥抢单的情景。

引言

一篇带你立马搞定消息中间件,并使用RabbitMQ模拟用户下单,外卖小哥抢单的情景。 这篇文章有点长哦,希望你耐着性子看完,然后会有所收获!!!!

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 的区别

ActiveMQRabbitMQRocketMQKafkaZeroMQ
单机吞吐量比RabbitMQ低2.6w/s(消息做持久化)11.6w/s17.3w/s29w/s
开发语言JavaErlangJavaScala/JavaC
主要维护者ApacheMozilla/SpringAlibabaApacheiMatix,创始人已去世
成熟度成熟成熟开源版本不够成熟比较成熟只有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工作流程图
黄色的圈圈就是我们的消息推送服务,将消息推送到 中间方框里面也就是 rabbitMq的服务器,然后经过服务器里面的交换机、队列等各种关系(后面会详细讲)将数据处理入列后,最终右边的蓝色圈圈消费者获取对应监听的消息。

一、simple模式(即最简单的收发模式)

simple模式
1.消息产生消息,将消息放入队列

2.消息的消费者(consumer) 监听 消息队列,如果队列中有消息,就消费掉,消息被拿走后,自动从队列中删除(隐患 消息可能没有被消费者正确处理,已经从队列中消失了,造成消息的丢失,这里可以设置成手动的ack,但如果设置成手动ack,处理完后要及时发送ack消息给队列,否则会造成内存溢出)。

二、work工作模式(资源的竞争)

work工作模式

1.消息产生者将消息放入队列消费者可以有多个,消费者1,消费者2同时监听同一个队列,消息被消费。C1 C2共同争抢当前的消息队列内容,谁先拿到谁负责消费消息(隐患:高并发情况下,默认会产生某一个消息被多个消费者共同使用,可以设置一个开关(syncronize) 保证一条消息只能被一个消费者使用)。

三、publish/subscribe发布订阅(共享资源)

publish/subscribe发布订阅
1、每个消费者监听自己的队列;

2、生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息。

四、routing路由模式

routing路由模式
1.消息生产者将消息发送给交换机按照路由判断,路由是字符串(info) 当前产生的消息携带路由字符(对象的方法),交换机根据路由的key,只能匹配上路由key对应的消息队列,对应的消费者才能消费消息;

2.根据业务功能定义路由字符串;

3.从系统的代码逻辑中获取对应的功能字符串,将消息任务扔到对应的队列中。

4.业务场景:error 通知;EXCEPTION;错误通知的功能;传统意义的错误通知;客户通知;利用key路由,可以将程序中的错误封装成消息传入到消息队列中,开发者可以自定义消费者,实时接收错误;

五、topic 主题模式(路由模式的一种)

topic 主题模式
1.星号井号代表通配符

2.星号代表多个单词,井号代表一个单词

3.路由功能添加模糊匹配

4.消息产生者产生消息,把消息交给交换机

5.交换机根据key的规则模糊匹配到对应的队列,由队列的监听消费者接收消息消费;

(在我的理解看来就是routing查询的一种模糊匹配,就类似sql的模糊查询方式)

RabbitMQ的交换机类型

常用的交换机有以下三种,因为消费者是从队列获取信息的,队列是绑定交换机的(一般),所以对应的消息推送/接收模式也会有以下几种:
Direct Exchange

一、直连型交换机

直连型交换机
根据消息携带的路由键将消息投递给对应队列。

大致流程,有一个队列绑定到一个直连交换机上,同时赋予一个路由键 routing key 。
然后当一个消息携带着路由值为X,这个消息通过生产者发送给交换机时,交换机就会根据这个路由值X去寻找绑定值也是X的队列。

二、Fanout Exchange

Fanout Exchange

扇型交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。 这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。

三、Topic 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.ConfirmCallbackRabbitTemplate.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模拟用户下单,外卖小哥抢单的情景。,谢谢~~~

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
中间件是指位于应用程序和操作系统之间的软件层,用于协调不同应用程序之间的通信和数据传输。RabbitMQ是一种流行的开源消息中间件,它实现了高级消息队列协议(AMQP)。 RabbitMQ的主要特点包括: 1. 可靠性:RabbitMQ使用消息确认机制来确保消息的可靠传递。发送方可以收到关于消息是否成功发送到队列的确认信息,并且接收方可以发送确认消息来告知RabbitMQ已经成功处理了消息。 2. 灵活的消息路由:RabbitMQ支持多种消息路由方式,包括直接路由、主题路由和扇出路由。这使得开发人员可以根据具体需求将消息发送到不同的队列交换机。 3. 高可用性:RabbitMQ支持集群模式,可以将多个节点组成一个集群,提供高可用性和负载均衡。 4. 消息持久化:RabbitMQ可以将消息持久化到磁盘,即使在服务器重启后也能保证消息的可靠性。 5. 多语言支持:RabbitMQ提供了多种编程语言的客户端库,包括Java、Python、C#等,方便开发人员在不同的平台上使用使用RabbitMQ时,你需要了解以下几个核心概念: 1. 生产者(Producer):负责发送消息到RabbitMQ。 2. 消费者(Consumer):负责从RabbitMQ接收消息并进行处理。 3. 队列(Queue):用于存储消息的容器,生产者将消息发送到队列,消费者从队列中接收消息。 4. 交换机(Exchange):用于接收生产者发送的消息,并将消息路由到一个或多个队列。 5. 绑定(Binding):用于将交换机队列进行绑定,定义了消息的路由规则。 6. 路由键(Routing Key):生产者在发送消息时指定的关键字,用于交换机将消息路由到相应的队列

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值