RabbitMQ入门

RabbitMQ解决了三个问题

1.流量消峰 举个例子,如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正 常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限 制订单超过一万后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分 散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体 验要好。

2.应用解耦 以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合 调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于 消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在 这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流 系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性。

3.异步处理 有些服务间调用是异步的,例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可 以执行完,以前一般有两种方式,A 过一段时间去调用 B 的查询 api 查询。或者 A 提供一个 callback api, B 执行完之后调用 api 通知 A 服务。这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题, A 调用 B 服务后,只需要监听 B 处理完成的消息,当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此 消息转发给 A 服务。这样 A 服务既不用循环调用 B 的查询 api,也不用提供 callback api。同样 B 服务也不 用做这些操作。A 服务还能及时的得到异步处理成功的消息。

六种模式

建立connection 开销比较打 Channel 是在connection内部建立的逻辑连接,以后可以直接和Channel进行通信

建立MQ的步骤

创建账号 rabbitmqctl add_user admin 123

设置用户角色 rabbitmqctl set_user_tags admin administrator

设置用户权限 set_permissions [-p ] rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*" 用户 user_admin 具有/vhost1 这个 virtual host 中所有资源的配置、写、读权限

当前用户和角色 rabbitmqctl list_users

JAVA发送消息          

package org.example;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

/**
 * @projectName: RabbitMQJava
 * @package: org.example
 * @className: Procuder
 * @author: Eric
 * @description: TODO
 * @date: 7/22/2023 3:19 PM
 * @version: 1.0
 */
public class Producer {
    private final static String QUEUE_NAME = "hello";
    public static void main(String[] args) throws Exception {
        //创建一个连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.232.146");
        factory.setUsername("admin");
        factory.setPassword("123");
        //channel 实现了自动 close 接口 自动关闭 不需要显示关闭
        try(
                Connection connection = factory.newConnection(); Channel channel =
                connection.createChannel()
        ) {
            /**
             * 生成一个队列
             * 1.队列名称
             * 2.队列里面的消息是否持久化 默认消息存储在内存中
             * 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
             * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
             * 5.其他参数
             */
            channel.queueDeclare(QUEUE_NAME,false,false,false,null);
            String message="hello world";
            /**
             * 发送一个消息
             * 1.发送到那个交换机
             * 2.路由的 key 是哪个
             * 3.其他的参数信息
             * 4.发送消息的消息体
             */
            channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
            System.out.println("消息发送完毕");
        }
    }

}

JAVA接收消息

package org.example;

import com.rabbitmq.client.*;

/**
 * @projectName: RabbitMQJava
 * @package: org.example
 * @className: Consumer
 * @author: Eric
 * @description: TODO
 * @date: 7/22/2023 3:20 PM
 * @version: 1.0
 */
public class Consumer {
    private final static String QUEUE_NAME = "hello";
    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.232.146");
        factory.setUsername("admin");
        factory.setPassword("123");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        System.out.println("等待接收消息....");
        //推送的消息如何进行消费的接口回调
        DeliverCallback deliverCallback=(consumerTag, delivery)->{
            String message= new String(delivery.getBody());
            System.out.println(message);
        };
        //取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback=(consumerTag)->{
            System.out.println("消息消费被中断");
        };
        /**
         * 消费者消费消息
         * 1.消费哪个队列
         * 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
         * 3.消费者未成功消费的回调
         */
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
    }


}

消息手动应答

channel.basicConsume(QUEUE_NAME,false,deliverCallback,cancelCallback);
//将第二个参数改为false为手动应答
//手动应答还需要在回调函数中添加
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);

只有应答之后该线程才能去处理其他信息

持久化

要想让消息实现持久化需要在消息生产者修改代码,MessageProperties.PERSISTENT_TEXT_PLAIN 添 加这个属性。

将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是 这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点。此时并没 有真正写入磁盘。持久性保证并不强。

不公平分发

在最开始的时候我们学习到 RabbitMQ 分发消息采用的轮训分发,但是在某种场景下这种策略并不是 很好,比方说有两个消费者在处理任务,其中有个消费者 1 处理任务的速度非常快,而另外一个消费者 2 处理速度却很慢,这个时候我们还是采用轮训分发的化就会到这处理速度快的这个消费者很大一部分时间 处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但是 RabbitMQ 并不知道这种情况它依然很公平的进行分发。

为了避免这种情况,我们可以设置参数 channel.basicQos(1);

意思就是如果这个任务我还没有处理完或者我还没有应答你,你先别分配给我,我目前只能处理一个 任务,然后 rabbitmq 就会把该任务分配给没有那么忙的那个空闲消费者,当然如果所有的消费者都没有完 成手上任务,队列还在不停的添加新任务,队列有可能就会遇到队列被撑满的情况,这个时候就只能添加 新的 worker 或者改变其他存储任务的策略。

发布确认原理

生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的 消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队 列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传 给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置 basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。

发布确认默认是没有开启的,如果要开启需要调用方法 confirmSelect,每当你要想使用发布 确认,都需要在 channel 上调用该方法

单个确认发布

public static void startRun01() throws Exception {
        String QUEUE_NAME = "d0c5307b-c7d6-4813-b4ed-aa8cf2fb6eac";
        Channel channel = RabbitMqUtils.getChannel();
        String name = UUID.randomUUID().toString();
        channel.queueDeclare(QUEUE_NAME,true,false,false,null);
        //开启发布确认
        channel.confirmSelect();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            channel.basicPublish("",QUEUE_NAME,null, String.valueOf(i).getBytes());


        }

        System.out.println("耗费时间"+(System.currentTimeMillis()-start)+"ms");

    }

多个确认发布

public static void startRun02() throws Exception {
        String QUEUE_NAME = "d1c5307b-c7d6-4813-b4ed-aa8cf2fb6eac";
        Channel channel = RabbitMqUtils.getChannel();
        String name = UUID.randomUUID().toString();
        channel.queueDeclare(QUEUE_NAME,true,false,false,null);
        //开启发布确认
        channel.confirmSelect();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            channel.basicPublish("",QUEUE_NAME,null, String.valueOf(i).getBytes());


        }
        boolean b = channel.waitForConfirms();
        if(b){
            System.out.println("消息发送成功");
        }
        System.out.println("耗费时间"+(System.currentTimeMillis()-start)+"ms");

    }

  1. 生产者开启确认模式:在 RabbitMQ 中,生产者可以通过调用 channel.confirmSelect() 方法来启用确认模式(Confirm Mode)。

  2. 生产者生产消息:启用了确认模式后,生产者可以通过 channel.basicPublish() 方法发送消息到 RabbitMQ 服务器。

  3. MQ 传递消息给消费者:RabbitMQ 服务器收到生产者发送的消息后,将其传递给消费者。

  4. 消费者处理消息并提交 ACK:消费者成功处理消息后,会向 RabbitMQ 服务器提交 ACK(确认)信号,表示消息已被成功处理。

  5. MQ 确认消息接收:RabbitMQ 服务器接收到来自消费者的 ACK 后,会立即确认这条消息的接收。这是由 RabbitMQ 内部自动处理的,无需生产者干预。

  6. 生产者等待确认:在生产者端,生产者可以通过 waitForConfirmsOrDie(long) 方法来等待 RabbitMQ 服务器的确认信号。这个方法会阻塞当前线程,直到所有先前发布的消息都被成功确认或者超时。

总结: 在这个过程中,生产者开启确认模式后,生产者发送消息给 RabbitMQ 服务器。MQ 将消息传递给消费者。消费者处理消息后,会向 MQ 提交 ACK 表示消息已被成功处理。RabbitMQ 服务器会自动确认消息的接收。生产者在需要的时候可以通过 waitForConfirmsOrDie(long) 方法等待消息成功发送到 RabbitMQ 服务器。

请注意,生产者在 waitForConfirmsOrDie(long) 方法中等待的是消息成功发送给 RabbitMQ 服务器的确认信号,并不是消费者的 ACK 状态。消费者的 ACK 是由 RabbitMQ 内部自动处理的,并不需要生产者主动查询。确认模式主要用于保证消息发送的可靠性,而与消费者的 ACK 无关。

public static void startRun03() throws Exception {
        String QUEUE_NAME = "test";
        Channel channel = RabbitMqUtils.getChannel();
        String name = UUID.randomUUID().toString();
        channel.queueDeclare(QUEUE_NAME,true,false,false,null);
        //开启发布确认
        channel.confirmSelect();
        //准备一个线程安全的队列
        ConcurrentSkipListMap<Long,String> concurrentSkipListMap = new ConcurrentSkipListMap<>();


        //准备消息监听器
        channel.addConfirmListener((deliveryTag, multiple) ->{
            //System.out.println("正确的信息"+deliveryTag);
            if (multiple) {
                //返回的是小于等于当前序列号的未确认消息 是一个 map
                ConcurrentNavigableMap<Long, String> confirmed =
                        concurrentSkipListMap.headMap(deliveryTag, true);
                //清除该部分未确认消息
                confirmed.clear();
            }else{
                //只清除当前序列号的消息
                concurrentSkipListMap.remove(deliveryTag);
            }
        },(deliveryTag, multiple) ->{
            String message = concurrentSkipListMap.get(deliveryTag);

            System.out.println("失败的消息"+deliveryTag);
        });
        long start = System.currentTimeMillis();

        for (int i = 0; i < 100; i++) {
            channel.basicPublish("",QUEUE_NAME,null, String.valueOf(i).getBytes());
            //记录所有要记录的消息
            concurrentSkipListMap.put(channel.getNextPublishSeqNo(),String.valueOf(i));
            //删除掉已经确认了的消息
            //剩下的就是未确认的消息
            //打印未确认的消息

        }
//        boolean b = channel.waitForConfirms();
//        if(b){
//            System.out.println("消息发送成功");
//        }

        System.out.println("耗费时间"+(System.currentTimeMillis()-start)+"ms");

    }


}

交换机

交换机的作用是分发消息给不同的队列

交换机接收生产者发送的消息,并根据消息的路由键(或消息的 headers 属性,具体取决于交换机类型)将消息路由到一个或多个队列中。不同类型的交换机具有不同的路由策略:

  • 直连交换机(Direct Exchange):根据消息的路由键将消息发送到与之绑定的队列中。路由键和队列名必须完全匹配。

  • 广播交换机(Fanout Exchange):将消息发送到与之绑定的所有队列中,忽略消息的路由键。

  • 主题交换机(Topic Exchange):根据消息的路由键与交换机和队列绑定的规则进行模式匹配,将消息发送到符合条件的队列中。

  • 头交换机(Headers Exchange):根据消息的 headers 属性进行匹配,决定将消息发送到哪个队列。

直连交换机

接收方

接收方需要声明自己连接的交换机,并且需要在声明队列的时候指明自己归属于哪个键

public class ReceiveLog02 {
    private static final String EXCHANGE_NAME = "direct_logs";
    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(EXCHANGE_NAME,  BuiltinExchangeType.DIRECT);
        channel.queueDeclare("disk",false,false,false,null);
        /**
         * 生成一个临时的队列 队列的名称是随机的
         * 当消费者断开和该队列的连接时 队列自动删除
         */

        //把该临时队列绑定我们的 exchange 其中 routingkey(也称之为 binding key)为空字符串
        channel.queueBind("disk", EXCHANGE_NAME, "error");
        System.out.println("等待接收消息,把接收到的消息写到文件.....");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            File file = new File("D:\\rabbitmq_info.txt");
            FileUtils.writeStringToFile(file, message, "UTF-8");
            System.out.println("数据写入文件成功");
        };
        channel.basicConsume("disk", true, deliverCallback, consumerTag -> {
        });
    }
}

发送方

发送方在发送消息的时候,也需要声明一个交换机,并且在公布消息的时候指明消息的键和值

public class Emit {
    private static final String EXCHANGE_NAME = "direct_logs";
    public static void main(String[] argv) throws Exception {
        try (Channel channel = RabbitMqUtils.getChannel()) {
            /**
             * 声明一个 exchange
             * 1.exchange 的名称
             * 2.exchange 的类型
             */
            channel.exchangeDeclare(EXCHANGE_NAME,  BuiltinExchangeType.DIRECT);
            Map<String, String> bindingKeyMap = new HashMap<>();
            bindingKeyMap.put("info","普通 info 信息");
            bindingKeyMap.put("warning","警告 warning 信息");
            bindingKeyMap.put("error","错误 error 信息");
            //debug 没有消费这接收这个消息 所有就丢失了
            bindingKeyMap.put("debug","调试 debug 信息");
            for (Map.Entry<String, String> bindingKeyEntry: bindingKeyMap.entrySet()){
                String bindingKey = bindingKeyEntry.getKey();
                String message = bindingKeyEntry.getValue();
                //发布消息的时候带上Key Value exchange_name
                channel.basicPublish(EXCHANGE_NAME,bindingKey, null,
                        message.getBytes(StandardCharsets.UTF_8));
                System.out.println("生产者发出消息:" + message);
            }
        }
    }


}

主题交换机

  • 直连交换机:在直连交换机中,一个消息只能被发送到一个队列中,即使有多个队列与交换机绑定,也只会被路由到其中一个队列。
  • 主题交换机:在主题交换机中,一个消息可以被发送到多个队列中,它根据消息的路由键和队列的绑定键进行模式匹配,将消息路由到所有匹配的队列。

主题交换机可以把一个信息发送给多个队列,比广播 直连更加灵活

*(星号) 可以代替一个单词

#(井号) 可以替代零个或多个单词

当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像 fanout

如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是 direct

如何构建呢

发送者

1. 先声明交换机类型为 主题模式

channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);


public class Emit {
    private static final String EXCHANGE_NAME = "topic_logs";
    public static void main(String[] argv) throws Exception {
        try (Channel channel = RabbitMqUtils.getChannel()) {
            /**
             * 声明一个 exchange
             * 1.exchange 的名称
             * 2.exchange 的类型
             */
            channel.exchangeDeclare(EXCHANGE_NAME,  BuiltinExchangeType.TOPIC);
            Map<String, String> bindingKeyMap = new HashMap<>();
            bindingKeyMap.put("quick.orange.rabbit","被队列 Q1Q2 接收到");
            bindingKeyMap.put("lazy.orange.elephant","被队列 Q1Q2 接收到");
            bindingKeyMap.put("quick.orange.fox","被队列 Q1 接收到");
            bindingKeyMap.put("lazy.brown.fox","被队列 Q2 接收到");
            bindingKeyMap.put("lazy.pink.rabbit","虽然满足两个绑定但只被队列 Q2 接收一次");
            bindingKeyMap.put("quick.brown.fox","不匹配任何绑定不会被任何队列接收到会被丢弃");
            bindingKeyMap.put("quick.orange.male.rabbit","是四个单词不匹配任何绑定会被丢弃");
            bindingKeyMap.put("lazy.orange.male.rabbit","是四个单词但匹配 Q2");

            for (Map.Entry<String, String> bindingKeyEntry: bindingKeyMap.entrySet()){
                String bindingKey = bindingKeyEntry.getKey();
                String message = bindingKeyEntry.getValue();
                //发布消息的时候带上Key Value exchange_name
                channel.basicPublish(EXCHANGE_NAME,bindingKey, null,
                        message.getBytes(StandardCharsets.UTF_8));
                System.out.println("生产者发出消息:" + message);
            }
        }
    }


}

接收者

1. 在声明队列之后对队列的主题进行绑定

channel.queueDeclare(queueName,false,false,false,null);
channel.queueBind(queueName, EXCHANGE_NAME, "*.orange.*");

其实和前面的写法差不多唯一的区别就是把键写成 "*.orange.*" 这样


public class ReceiveLog01 {
    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) throws Exception {
        String queueName = "console";
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(EXCHANGE_NAME,  BuiltinExchangeType.TOPIC);
        channel.queueDeclare(queueName,false,false,false,null);
        channel.queueBind(queueName, EXCHANGE_NAME, "*.orange.*");
        /**
         * 生成一个临时的队列 队列的名称是随机的
         * 当消费者断开和该队列的连接时 队列自动删除
         */

        //把该临时队列绑定我们的 exchange 其中 routingkey(也称之为 binding key)为空字符串
        channel.queueBind("console", EXCHANGE_NAME, "info");
        channel.queueBind("console", EXCHANGE_NAME, "warning");
        System.out.println("等待接收消息,把接收到的消息打印在屏幕.....");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("控制台打印接收到的消息"+message);
        };
        channel.basicConsume("console", true, deliverCallback, consumerTag -> { });
    }
}

死信

在消息队列(MQ)中,死信(Dead Letter)是指那些无法被正常消费并且不能进一步处理的消息。当消息被标记为死信后,它们通常会被转移到一个特殊的队列,称为死信队列(Dead-Letter Queue,DLQ)。这样可以使无法被处理的消息得到处理或分析,而不会导致消息被永久地丢弃。

四种情况会出现死信

  1. 消息被拒绝:当消费者在手动 ACK 模式下处理消息时,如果消费者明确拒绝处理该消息(调用 basicRejectbasicNack),消息可能会被标记为死信。

  2. 消息过期:消息可能具有一定的生存时间,在到达一定时间后仍未被消费,消息可能会被标记为死信。

  3. 消费者无法处理:当消息被投递给消费者后,如果消费者一直处于忙碌状态或处理异常,导致消息无法被成功处理,消息可能会被标记为死信。

  4. 队列溢出:当队列的长度超过了一定的阈值,在新消息到达队列时,旧的消息可能会被标记为死信。

接下来的代码实战有点小复杂

消费者超时处理

consumer01

1. 声明死信交换机/普通交换机

channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

2. 普通队列声明绑定普通交换机/死心队列声明绑定死信交换机

channel.queueDeclare(DEAD_QUEUE,false,false,false,null);
channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "lisi");

channel.queueDeclare(NORMAL_QUEUE,false,false,false,params);
channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "zhangsan");

3. 注册普通队列的消费者

channel.queueDeclare(NORMAL_QUEUE,false,false,false,params);

4. 将死信的设置以参数的形式传入普通消费者

Map<String, Object> params = new HashMap<>();
//正常队列设置死信交换机 参数 key 是固定值
params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
params.put("x-dead-letter-routing-key", "lisi");

//普通交换机和普通队列进行捆绑
channel.queueDeclare(NORMAL_QUEUE,false,false,false,params);

如果普通队列出现了死信就会按照下面设置的东西进行死信传值

params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
params.put("x-dead-letter-routing-key", "lisi");

public class Consumer01 {
    //普通交换机名称
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    //死信交换机名称
    private static final String DEAD_EXCHANGE = "dead_exchange";

    private static final String NORMAL_QUEUE = "normal_queue";
    private static final String DEAD_QUEUE = "dead_queue";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        DeliverCallback deliverCallback = (consumerTag,message)->{
            System.out.println("正常队列:"+new String(message.getBody()));

        };
        //死信交换机和死信队列进行捆绑
        channel.queueDeclare(DEAD_QUEUE,false,false,false,null);
        channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "lisi");

        //消费者注册NORMAL_QUEUE
        channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,consumerTag -> {});

        Map<String, Object> params = new HashMap<>();
        //正常队列设置死信交换机 参数 key 是固定值
        params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
        params.put("x-dead-letter-routing-key", "lisi");

        //普通交换机和普通队列进行捆绑
        channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "zhangsan");
        //NORMAL_QUEUE声明的时候指定他的死信队列参数
        channel.queueDeclare(NORMAL_QUEUE,false,false,false,params);




    }



}

Customer02

1. 注册死信队列

public class Consumer02 {

    private static final String DEAD_QUEUE = "dead_queue";
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        DeliverCallback deliverCallback = (consumerTag,message)->{
            System.out.println("正常队列:"+new String(message.getBody()));

        };
        channel.basicConsume(DEAD_QUEUE,true,deliverCallback,a->{});


    }


}

produce

1. 向普通队列发送消息

public class Produce {
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    public static void main(String[] argv) throws Exception {
        try (Channel channel = RabbitMqUtils.getChannel()) {

            //设置消息的 TTL 时间
            AMQP.BasicProperties properties = new
                    AMQP.BasicProperties().builder().expiration("10000").build();
            //该信息是用作演示队列个数限制
            for (int i = 1; i <11 ; i++) {
                String message="info"+i;
                channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", properties,
                        message.getBytes());
                System.out.println("生产者发送消息:"+message);
            }
        }
    }



}

可以先打开Customer1 然后再关闭 就为了注册相关交换机和配置死信队列

然后打开Customer02

打开produce 

就可以看到消息因为Customer1 的死亡全部转发到Customer02(死信队列里面去了)

这个成为死信的原因是消费者超时处理

mq队列达到最大

Consumer01

1. 声明队列最大数量

params.put("x-max-length", 6);

public class Consumer01 {
    //普通交换机名称
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    //死信交换机名称
    private static final String DEAD_EXCHANGE = "dead_exchange";

    private static final String NORMAL_QUEUE = "normal_queue";
    private static final String DEAD_QUEUE = "dead_queue";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        DeliverCallback deliverCallback = (consumerTag,message)->{
            System.out.println("正常队列:"+new String(message.getBody()));

        };
        //死信交换机和死信队列进行捆绑
        channel.queueDeclare(DEAD_QUEUE,false,false,false,null);
        channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "lisi");
        Map<String, Object> params = new HashMap<>();
        //正常队列设置死信交换机 参数 key 是固定值
        params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
        params.put("x-dead-letter-routing-key", "lisi");
        params.put("x-max-length", 6);
        //普通交换机和普通队列进行捆绑
        channel.queueDeclare(NORMAL_QUEUE,false,false,false,params);
        channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "zhangsan");
        //消费者注册NORMAL_QUEUE
        channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,consumerTag -> {});



        //NORMAL_QUEUE声明的时候指定他的死信队列参数


    }



}

消息被拒绝

Consumer01

1. 将自动提交ack设置为false 

2. 重写回调接口

DeliverCallback deliverCallback = (consumerTag,message)->{

            if(new String(message.getBody()).equals("info5")){
                System.out.println("Consumer01 接收到消息" + new String(message.getBody()) + "并拒绝签收该消息");
                //requeue 设置为 false 代表拒绝重新入队 该队列如果配置了死信交换机将发送到死信队列中
                channel.basicReject(message.getEnvelope().getDeliveryTag(), false);
            }else {
                System.out.println("Consumer01 接收到消息"+new String(message.getBody()));
                channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
            }
        };

延迟队列

延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望 在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的 元素的队列。

1.订单在十分钟之内未支付则自动取消

2.新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。

3.用户注册成功后,如果三天内没有登陆则进行短信提醒。

4.用户发起退款,如果三天内没有得到处理则通知相关运营人员。

5.预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议

SpringBoot中的MQ

普通版

1. 注册相关交换机 队列

和之前的创建方式有些不同 这里的创建直接调用了构造方法 需要另外记一下

@Configuration
public class TtQueueConfig {

    //普通交换机的名称
    public static final String X_EXCHANGE = "X";
    //死信交换机名称
    public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
    //普通队列
    public static final String A_QUEUE = "QA";
    public static final String B_QUEUE = "QB";
    //死信队列
    public static final String DEAD_LETTER_QUEUE = "QD";

    //声明X交换机
    @Bean("X_EXCHANGE")
    public DirectExchange X_exchange(){
        return new DirectExchange(X_EXCHANGE);
    }
    //声明X交换机
    @Bean("Y_DEAD_LETTER_EXCHANGE")
    public DirectExchange Y_exchange(){
        return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
    }

    @Bean("A_QUEUE")
    public Queue A_QUEUE(){
        HashMap<String, Object> map = new HashMap<>();
        //设置死信交换机
        map.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
        //设置死信Key
        map.put("x-dead-letter-routing-key","YD");
        //设置过期时间
        map.put("x-message-ttl",10000);
        return QueueBuilder.durable(A_QUEUE).withArguments(map).build();
    }
    @Bean("B_QUEUE")
    public Queue B_QUEUE(){
        HashMap<String, Object> map = new HashMap<>();
        //设置死信交换机
        map.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
        //设置死信Key
        map.put("x-dead-letter-routing-key","YD");
        //设置过期时间
        map.put("x-message-ttl",40000);
        return QueueBuilder.durable(B_QUEUE).withArguments(map).build();
    }

    @Bean("DEAD_LETTER_QUEUE")
    public Queue Y_DEAD_LETTER_QUEUE(){
        return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();
    }

    @Bean
    public Binding queueABindingX(@Qualifier("A_QUEUE") Queue queueA, @Qualifier("X_EXCHANGE")DirectExchange XExchange){
        return BindingBuilder.bind(queueA).to(XExchange).with("XA");
    }

    @Bean
    public Binding queueBBindingX(@Qualifier("B_QUEUE") Queue queueB, @Qualifier("X_EXCHANGE")DirectExchange XExchange){
        return BindingBuilder.bind(queueB).to(XExchange).with("XB");
    }

    @Bean
    public Binding queueDBindingX(@Qualifier("DEAD_LETTER_QUEUE") Queue queueD, @Qualifier("Y_DEAD_LETTER_EXCHANGE")DirectExchange Y_DEAD_LETTER_EXCHANGE){
        return BindingBuilder.bind(queueD).to(Y_DEAD_LETTER_EXCHANGE).with("YD");
    }



}

2. 死信队列

@Slf4j
@Component
public class DeadLetterQueueConsumer {

    @RabbitListener(queues = "QD")
    public void receiveD(Message message, Channel channel){
        String msg = new String(message.getBody());
        log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);

    }


}

3. 生产者

@Slf4j
@RestController
@RequestMapping("/ttl")
public class SendMsgController {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @GetMapping("/sendMsg/{message}")
    public void sendMsg(@PathVariable String message){

        log.info("当前时间:{},发送一条信息给两个 TTL 队列:{}", new Date(), message);
        rabbitTemplate.convertAndSend("X", "XA", "消息来自 ttl 为 10S 的队列: "+message);
        rabbitTemplate.convertAndSend("X", "XB", "消息来自 ttl 为 40S 的队列: "+message);


    }

}

改进版

在增加一个QC队列,这个队列的过期时间可以根据用户设置动态调整

@GetMapping("sendExpirationMsg/{message}/{ttlTime}")
    public void sendMsg(@PathVariable String message,@PathVariable String ttlTime) {
        rabbitTemplate.convertAndSend("X", "XC", message, correlationData ->{
            correlationData.getMessageProperties().setExpiration(ttlTime);
            return correlationData;
        });
        log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(),ttlTime, message);
    }

出现了问题 

消息1 不解决完消息2 不会被发送

消息2 等待了12s才被死信

使用插件

将消息延迟的功能交给交换机去完成

在官网上下载 https://www.rabbitmq.com/community-plugins.html,下载 rabbitmq_delayed_message_exchange 插件,然后解压放置到 RabbitMQ 的插件目录。

rabbitmq-plugins enable rabbitmq_delayed_message_exchange  进行安装

public class DelayedQueueConfig {
    public static final String DELAYED_QUEUE_NAME = "delayed.queue";
    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
    public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
    @Bean
    public Queue delayedQueue() {
        return new Queue(DELAYED_QUEUE_NAME);
    }
    //自定义交换机 我们在这里定义的是一个延迟交换机
    @Bean
    public CustomExchange delayedExchange() {
        Map<String, Object> args = new HashMap<>();
        //自定义交换机的类型
        args.put("x-delayed-type", "direct");
        return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false,
                args);
    }
    @Bean
    public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue,
                                       @Qualifier("delayedExchange") CustomExchange
                                               delayedExchange) {
        return
                BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
    }

}

主要是使用了新的交换机

声明交换机的时候要填入姓名 类型 持久化 自动删除 参数

new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false,args);
 

发布确认高级

在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败, 导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢? 特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢:

在配置文件当中需要添加 spring.rabbitmq.publisher-confirm-type=correlated

⚫ NONE 禁用发布确认模式,是默认值

⚫ CORRELATED 发布消息成功到交换器后会触发回调方法

⚫ SIMPLE 经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法

交换机出问题

怎么写

1. 配置文件添加

spring.rabbitmq.publisher-confirm-type=correlated

2. 重写回调接口

public class MyCallBack implements RabbitTemplate.ConfirmCallback

3. 初始化消费者的时候指定回调函数

@PostConstruct
public void init(){
        rabbitTemplate.setConfirmCallback(myCallBack);
}

4. 生产者发送消息添加相关性数据

CorrelationData correlationData = new CorrelationData("1");
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME+"1","key1",message,correlationData);

配置文件

三步走

1. 声明交换机

2. 声明队列

3. 声明绑定关系

@Configuration
public class ConfirmConfig {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
    //声明业务 Exchange
    @Bean("confirmExchange")
    public DirectExchange confirmExchange(){
        return new DirectExchange(CONFIRM_EXCHANGE_NAME);
    }
    // 声明确认队列
    @Bean("confirmQueue")
    public Queue confirmQueue(){
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }
    // 声明确认队列绑定关系
    @Bean
    public Binding queueBinding(@Qualifier("confirmQueue") Queue queue,
                                @Qualifier("confirmExchange") DirectExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("key1");
    }
}

生产者

@GetMapping("/sendMessage/{message}")
    public void sendMessage(@PathVariable String message){
        CorrelationData correlationData = new CorrelationData("1");
        rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME+"1","key1",message,correlationData);

        log.info("发送消息内容为:{}",message);

    }

回调接口

@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
    /**
     * 交换机不管是否收到消息的一个回调方法
     * CorrelationData
     * 消息相关数据
     * ack
     * 交换机是否收到消息
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id=correlationData!=null?correlationData.getId():"";
        if(ack){
            log.info("交换机已经收到 id 为:{}的消息",id);
        }else{
            log.info("交换机还未收到 id 为:{}消息,由于原因:{}",id,cause);
        }
    }
}

消费者

@Component
@Slf4j
public class Consumer {
    @RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
    public void receiveConfirmMessage(Message message){
        String msg = new String(message.getBody());

        log.info("交换机已经收到 为:{}的消息",msg);


    }

}

队列出问题

怎么写

1. 回调函数添加returnMessage

@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {
    /**
     * 交换机不管是否收到消息的一个回调方法
     * CorrelationData
     * 消息相关数据
     * ack
     * 交换机是否收到消息
     */
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
        rabbitTemplate.setMandatory(true);
    }
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData != null ? correlationData.getId() : "";
        if (ack) {
            log.info("交换机收到消息确认成功, id:{}", id);
        } else {
            log.error("消息 id:{}未成功投递到交换机,原因是:{}", id, cause);
        }
    }
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String
            exchange, String routingKey) {
        log.info("消息:{}被服务器退回,退回原因:{}, 交换机是:{}, 路由 key:{}",
                new String(message.getBody()),replyText, exchange, routingKey);
    }
}

备份交换机

可以把队列中出现问题的信息送到备份交换机,由备份交换机发送到备份/警告队列

 配置交换机和队列的代码

@Configuration
public class ConfirmConfig {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
    public static final String BACKUP_EXCHANGE_NAME = "backup.exchange";
    public static final String BACKUP_QUEUE_NAME = "backup.queue";
    public static final String WARNING_QUEUE_NAME = "warning.queue";
    //声明备份 Exchange
    // 声明确认队列
    @Bean("confirmQueue")
    public Queue confirmQueue(){
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }
    //声明确认队列绑定关系
    @Bean
    public Binding queueBinding(@Qualifier("confirmQueue") Queue queue,
                                @Qualifier("confirmExchange") DirectExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("key1");
    }
    //声明备份 Exchange
    @Bean("backupExchange")
    public FanoutExchange backupExchange(){
        return new FanoutExchange(BACKUP_EXCHANGE_NAME);
    }
    //声明确认 Exchange 交换机的备份交换机
    @Bean("confirmExchange")
    public DirectExchange confirmExchange(){
        ExchangeBuilder exchangeBuilder =
                ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
                        .durable(true)
                        //设置该交换机的备份交换机
                        .withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME);
        return (DirectExchange)exchangeBuilder.build();
    }
    // 声明警告队列
    @Bean("warningQueue")
    public Queue warningQueue(){
        return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
    }
    // 声明报警队列绑定关系
    @Bean
    public Binding warningBinding(@Qualifier("warningQueue") Queue queue,
                                  @Qualifier("backupExchange") FanoutExchange
                                          backupExchange){
        return BindingBuilder.bind(queue).to(backupExchange);
    }
    // 声明备份队列
    @Bean("backQueue")
    public Queue backQueue(){
        return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
    }
    // 声明备份队列绑定关系
    @Bean
    public Binding backupBinding(@Qualifier("backQueue") Queue queue,
                                 @Qualifier("backupExchange") FanoutExchange backupExchange){
        return BindingBuilder.bind(queue).to(backupExchange);
    }
}

消费者

//接收报警消息

    @RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
    public void receiveWarningMsg(Message message){

        String Msg = new String(message.getBody());
        log.error("报警发现不可路由消息:{}", Msg);

    }

MQ幂等性问题

用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。 举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常, 此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱 了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误 立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等

唯一 ID+指纹码机制

指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基 本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id 是否存 在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是单个数 据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。

Redis 原子性

利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费

优先级队列

在我们系统中有一个订单催付的场景,我们的客户在天猫下的订单,淘宝会及时将订单推送给我们,如 果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧,但是,tmall 商家对我们来说,肯定是要分大客户和小客户的对吧,比如像苹果,小米这样大商家一年起码能给我们创 造很大的利润,所以理应当然,他们的订单必须得到优先处理,而曾经我们的后端系统是使用 redis 来存 放的定时轮询,大家都知道 redis 只能用 List 做一个简简单单的消息队列,并不能实现一个优先级的场景, 所以订单量大了后采用 RabbitMQ 进行改造和优化,如果发现是大客户的订单给一个相对比较高的优先级, 否则就是默认优先级。

惰性队列

消息不存在内存中,而是存在于磁盘中

在执行性能下不是很好

FederationExchange

(broker 北京),(broker 深圳)彼此之间相距甚远,网络延迟是一个不得不面对的问题。有一个在北京 的业务(Client 北京) 需要连接(broker 北京),向其中的交换器 exchangeA 发送消息,此时的网络延迟很小, (Client 北京)可以迅速将消息发送至 exchangeA 中,就算在开启了 publisherconfirm 机制或者事务机制的 情况下,也可以迅速收到确认信息。此时又有个在深圳的业务(Client 深圳)需要向 exchangeA 发送消息, 那么(Client 深圳) (broker 北京)之间有很大的网络延迟,(Client 深圳) 将发送消息至 exchangeA 会经历一 定的延迟,尤其是在开启了 publisherconfirm 机制或者事务机制的情况下,(Client 深圳) 会等待很长的延 迟时间来接收(broker 北京)的确认信息,进而必然造成这条发送线程的性能降低,甚至造成一定程度上的 阻塞。 将业务(Client 深圳)部署到北京的机房可以解决这个问题,但是如果(Client 深圳)调用的另些服务都部 署在深圳,那么又会引发新的时延问题,总不见得将所有业务全部部署在一个机房,那么容灾又何以实现? 这里使用 Federation 插件就可以很好地解决这个问题

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值