RabbitMQ实战知识详细笔记,消息丢失,重复消费等问题解决方案。

本文深入探讨了RabbitMQ的使用,包括基本的简单模式、工作模式(解决消息丢失问题)、发布/订阅模式、路由模式、主题模式。此外,介绍了如何设置消息持久化、不公平分发和预取值。在SpringBoot中整合RabbitMQ,实现延时队列和发布确认模式。最后,讨论了幂等性、备份交换机和惰性队列等高级话题。
源码(码云):https://gitee.com/yin_zhipeng/rabbit-mq-demo.git

安装rabbitmq,可以参考,https://blog.youkuaiyun.com/grd_java/article/details/119696892,其它的内容没必要参考这篇文章

本文根据官方文档的6大模式,依次将知识点带出,比如工作模式中,多个消费者消费消息的消息丢失问题,可以通过手动应答解决。

一、相关概念和介绍

应用场景

  1. 流量削峰:如果系统的处理能力无法满足用户的请求数量。可能会导致系统瘫痪。虽然可以当系统处理能力达到上限时,限制用户请求,让用户无法操作。此时消息队列可以做一个缓冲,让这些请求分散开排好队,依次的进行处理。但是消息队列会
  2. 应用解耦:消息队列可以异步的执行任务,比如要买菜,买肉,不用异步的话,需要先买肉,然后等肉切好,再去买菜。解耦后,先买肉,让肉先切着,我同时去买菜。如果菜买回来,肉没切完,那就等肉。如果肉腐烂了,坏了。那可以重新挑肉,或者直接退钱,回家告诉孩子,今天做不了饭,肉腐烂了。
  3. 异步处理:可以想象为,你在等水烧开的时候,顺便扫地。

常见MQ

  1. ActiveMQ
  1. 优点:单机吞吐量万级,时效性ms级,可用性高,基于主从架构实现高可用,消息可靠性较高(不容易丢失数据)
  2. 缺点:比较老了,官方社区(Apache)对ActiveMQ 5.x维护越来越少,高吞吐量场景使用较少。
  1. Kafka:为大数据而生,百万级TPS的吞吐量,让它在数据采集,传输,存储过程中发挥出色,被LinkedIn,Uber,Twitter,Netflix等大公司采纳。
  1. 优点:性能卓越,单机写入TPS约百万条/秒,吞吐量高。时效性ms级,可用性非常高(分布式,一个数据多个副本,少数机器宕机,不会数据丢失,或不可用),消息有序,通过控制可以保证所有消息被消费且仅被消费一次,日志领域比较成熟,功能较为简单,支持简单的MQ功能,大数据领域的实时计算和日志采集被大规模使用。
  2. 缺点:队列越多,load越高,消息发送响应时间越长,短轮询方式,实时性取决于轮询时间间隔,消费失败不支持重试,一台代理宕机,消息会乱序,社区更新慢。
  1. RocketMQ:阿里巴巴开源产品,Java实现,参考Kafka并改进,广泛应用与订单,交易,充值,流计算,消息推送,日志流式处理,binglog分发等场景。
  1. 优点:单机吞吐量十万级,可用性高,分布式架构,消息可以做到0丢失,功能较为完善,扩展性好,支持10亿级别消息堆积。
  2. 缺点:支持的客户端语言少,目前只有java和c++,c++不成熟,社区活跃度一般,没有在MQ核心中实现JMS等接口,系统要迁移,有些需要修改大量代码。
  1. RabbitMQ:2007年发布,基于AMQP(高级消息队列协议),可复用企业消息系统,当前最主流的消息中间件之一。
  1. 优点:erlang语言编写(因此高并发特性好,性能好,吞吐量万级),MQ功能比较完备,健壮,稳定,易用,跨平台,支持多种语言,支持AJAX文档齐全,管理界面好用,社区活跃度高,更新频率高。
  2. 缺点:贵,商业版需要收费,学习成本高。

1. 四大核心

在这里插入图片描述

  1. 生产者:就是生产消息的,也就是发快递的,消息可以想象为快递。
  2. RabbitMQ:也就是快递站,处理快递,具体邮寄到哪里
  1. 交换机:邮寄的策略管理,主要管理一张路由表,比如这个快递要发到北京东城区,它会匹配路由表,然后根据管理策略,找合适的快递员(队列)。比如:“我这里有个包裹,你们谁来邮寄配送一下?”,再比如“1号队列,你现在还能处理包裹吗?满了是吧!2号队列,你能吗?可以是吧?给你!!!”
  2. 队列:就是运输快递的快递员的车,它会将包裹交到消费者手里。交换机管理多个快递员,当然这些快递员需要是自己家的快递员(具有绑定关系的队列)。比如顺丰快递不能管人家韵达快递的快递员。
  1. 消费者:也就是等快递的你,快递来了,你自己处理,直接用了,或者回馈一些,评个论,退个货啥的。

2. 名词介绍

在这里插入图片描述

  1. Borker:就是RabbitMQ实体,接收和分发消息的应用。RabbitMQ Server就是Message Broker
  2. Virtual host:虚拟主机,多个不同用户使用同一个RabbitMQ server提供的服务时,可以划分多个vhost,每个用户在自己的vhost创建exchange/queue等。是出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中,类似网络中的namespace概念。
  3. Connection:publisher/consumer和broker之间的TCP连接
  4. Channel:建立TCP连接开销很大,效率也低,Channel是TCP Connection内部建立的逻辑链接,多线程下,每个线程单独创建channel通讯(AMQP method中包含channel id帮助客户端和message broker识别channel),channel之间完全隔离,非常的轻量级。不用每次访问RabbitMQ都建立TCP connection。
  5. Exchange:message到达broker第一站,根据分发规则,匹配查询表中的routing key,分发消息到queue中,常用类型direct(点到点)、topic(发布-订阅)、fanout(广播)
  6. Queue:快递员的车,消息都在这里,等consumer消费者取走。
  7. Binding:exchange和queue之间的虚拟链接,也就是路由表,可以包含路由key(routing key)和Binding绑定信息,保存到exchange交换机的路由表中,用于message的分发依据

二、6大模式的前5个

1. 简单模式

在这里插入图片描述
整体架构为一个生产者P(Producer),消息队列(不用交换机,就用一个队列),和一个消费者C(consumer),效果就是P发给队列一个消息,C取一个消息

  1. 代码位置
    在这里插入图片描述
  2. 模块依赖
    <dependencies>
        <!--rabbitMq依赖客户端-->
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.7.3</version>
        </dependency>
        <!--操作文件流工具-->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>
    </dependencies>
  1. 生产者Producer代码:连接到mq,然后生成一个队列,然后把消息发送到队列即可
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.nio.charset.StandardCharsets;
/**
 * 生产者
 */
public class Producer {
   
   
    //交换机名词
    public static final String EXCHANGE_NAME = "simpleExchange";
    //队列名称
    public static final String QUEUE_NAME = "simpleModel";
    //路由key
    public static final String ROUTING_KEY = "simpleRoutingKey";
    //发消息
    public static void main(String[] args) throws Exception {
   
   
        //连接工厂,设计模式,可以方便我们进行配置
        ConnectionFactory f = new ConnectionFactory();
        //IP,连接RabbitMQ的队列,RabbitMQ是一个程序,我们需要连接使用它
        f.setHost("127.0.0.1");
        //用户名和密码,登录RabbitMQ的用户名和密码
        f.setUsername("guest");
        f.setPassword("guest");
        //创建连接,根据工作原理,我们消费者和生产者都是通过连接,和MQ通信
        Connection connection = f.newConnection();
        //获取信道,根据工作原理,TCP连接不断开启关闭非常消耗资源,因此提供逻辑信道,连接一直建立(只建立一次),而Producer和Consumer每次连接MQ都通过信道,提高效率
        Channel channel = connection.createChannel();
        /**
         * 跳过交换机直接生成队列
         * Params:参数
         * queue – the name of the queue队列名
         * durable – true if we are declaring a durable queue (the queue will survive a server restart)
         *          true表示声明持久队列,重启mq后,队列依然存在,但是队列里面的数据,不会持久化
         *          默认为false,存储在内存中
         * exclusive – true if we are declaring an exclusive queue (restricted to this connection)
         *          true表示声明独占队列,这个队列只属于此连接,不可以有多个消费者进行消费
         * autoDelete – true if we are declaring an autodelete queue (server will delete it when no longer in use)
         *          true表示当队列不再使用(最后一个消费者断开连接),将自动删除队列,
         * arguments – other properties (construction arguments) for the queue
         *          队列其它属性(构造参数),比如延迟消息等,是后面难度更高的内容
         */
        channel.queueDeclare(QUEUE_NAME,true,false,false,null);
        /**
         * 发布消息----注意routingKey,简单模式中,没有交换机,routingKey就是队列名字
         * Params: 参数
         * exchange – the exchange to publish the message to 要发送消息的交换机
         * routingKey – the routing key 路由key
         * XXXX一般不配置此项 mandatory – true if the 'mandatory' flag is to be set
         *  true表示强制的,一般不会配置此项
         * XXXX一般不配置此项 immediate – true if the 'immediate' flag is to be set. Note that the RabbitMQ server does not support this flag.
         *  true表示立刻及时的,注意RabbitMQ不支持此标签,一般不进行配置
         * props – other properties for the message - routing headers etc
         *      消息的其它参数,路由报头等
         * body – the message body 发送消息的消息体
         */
        channel.basicPublish("",QUEUE_NAME,null,"消息".getBytes(StandardCharsets.UTF_8));
        System.out.println("消息发送完毕!!!");
    }
}
  1. 消费者Consumer代码:连接到mq,指定队列,然后消费消息即可。
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Delivery;


/**
 * 消费者
 */
public class Consumer {
   
   
    //交换机名词
    public static final String EXCHANGE_NAME = "simpleExchange";
    //队列名称
    public static final String QUEUE_NAME = "simpleModel";
    //路由key
    public static final String ROUTING_KEY = "simpleRoutingKey";
    //消费消息
    public static void main(String[] args) throws Exception {
   
   
        //连接工厂,设计模式,可以方便我们进行配置
        ConnectionFactory f = new ConnectionFactory();
        //IP,连接RabbitMQ的队列,RabbitMQ是一个程序,我们需要连接使用它
        f.setHost("127.0.0.1");
        //用户名和密码,登录RabbitMQ的用户名和密码
        f.setUsername("guest");
        f.setPassword("guest");
        //创建连接,根据工作原理,我们消费者和生产者都是通过连接,和MQ通信
        Connection connection = f.newConnection();
        //获取信道,根据工作原理,TCP连接不断开启关闭非常消耗资源,因此提供逻辑信道,连接一直建立(只建立一次),而Producer和Consumer每次连接MQ都通过信道,提高效率
        Channel channel = connection.createChannel();
        /**
         * 消费消息
         * Params:
         * queue – the name of the queue 队列名
         * autoAck – true if the server should consider messages acknowledged once delivered; false if the server should expect explicit acknowledgements
         *      ture表示自动应答,也就是消息成功发送后,自动应答,确认消费成功。false表示不自动应答,需要手动进行应答,实战都是手动应答的。
         *      如果消息丢失情况出现,需要我们进行逻辑处理后,手动进行应答,告诉人家失败了,如果自动应答我们就没办法控制了
         * deliverCallback – callback when a message is delivered
         *      回调(建立上下文),消费者成功接收到消息
         *      一个函数式接口,需要匿名内部类,jdk1.8后,可以使用lambda表达式
         * cancelCallback – callback when the consumer is cancelled
         *      回调,消费者取消消费的回调
         *      一个函数式接口,需要匿名内部类,jdk1.8后,可以使用lambda表达式
         */
        channel.basicConsume(QUEUE_NAME, true, (String consumerTag, Delivery message) -> {
   
   
            System.out.println("消费者消费成功!!!"+consumerTag+"----------------消息为:"+new String(message.getBody()));
        }, (String consumerTag) -> {
   
   
            System.out.println("消费者取消消费或被中断!!!"+consumerTag);
        });
    }
}

2. 工作模式(解决消息丢失问题)

在这里插入图片描述
整体架构为一个Producer生产者,一个队列,多个Consumer消费者。整体效果为,P生产多个消息到队列,C抢夺消息进行消费,实现多个线程C同时处理队列中的消息,节省时间。注意:多个C之间是竞争关系,队列中消息每个只能处理一次,多个C采用轮询的策略,依次处理队列中消息(如果一共就两个消费者C1和C2.那么C1先处理一个,然后C2处理,然后再C1处理,以此类推)。

搞一个工具类,用来获取channel信道
在这里插入图片描述

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
 * 封装建立RabbitMq连接工厂的工具类
 */
public class RabbitMqUtils {
   
   
    //交换机名词
    public static final String EXCHANGE_NAME = "workQueuesExchange";
    //队列名称
    public static final String QUEUE_NAME = "workQueuesModel";
    //路由key
    public static final String ROUTING_KEY = "workQueuesRoutingKey";
    //得到一个channel
    public static Channel getChannel() throws Exception{
   
   
        //连接工厂,设计模式,可以方便我们进行配置
        ConnectionFactory f = new ConnectionFactory();
        //IP,连接RabbitMQ的队列,RabbitMQ是一个程序,我们需要连接使用它
        f.setHost("127.0.0.1");
        //用户名和密码,登录RabbitMQ的用户名和密码
        f.setUsername("guest");
        f.setPassword("guest");
        //创建连接,根据工作原理,我们消费者和生产者都是通过连接,和MQ通信
        Connection connection = f.newConnection();
        //获取信道,根据工作原理,TCP连接不断开启关闭非常消耗资源,因此提供逻辑信道,连接一直建立(只建立一次),而Producer和Consumer每次连接MQ都通过信道,提高效率
        Channel channel = connection.createChannel();
        return channel;
    }
}

2.1 效果实现

  1. 代码位置
    在这里插入图片描述
  2. 生产者代码:生产多条消息到队列
    在这里插入图片描述
import com.rabbitmq.client.Channel;
import com.yzpnb.rabbitmq.util.RabbitMqUtils;

import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * 生产者
 */
public class Producer {
   
   
    //发消息
    public static void main(String[] args) throws Exception {
   
   

        Channel channel = RabbitMqUtils.getChannel();
        /**
         * 跳过交换机直接生成队列
         */
        channel.queueDeclare(RabbitMqUtils.QUEUE_NAME,true,false,false,null);
        /**
         * 发布消息
         */
        Scanner scanner = new Scanner(System.in);
        while(true){
   
   
            String msg = scanner.nextLine();
            channel.basicPublish("",RabbitMqUtils.QUEUE_NAME,null,msg.getBytes(StandardCharsets.UTF_8));
            System.out.println("消息\""+msg+"\"发送完毕!!!");
        }

    }
}
  1. 消费者代码,多个消费者消费消息
    在这里插入图片描述
import com.rabbitmq.client.Channel;
import com.yzpnb.rabbitmq.util.RabbitMqUtils;

/**
 * 消费者,这里用两个线程代表两个消费者工作线程
 */
public class Consumer {
   
   
    public static void main(String[] args) {
   
   
        Thread[] threads = new Thread[2];
        for (int i = 0; i < 2; i++) {
   
   
            int finalI = i;
            threads[i]=
                new Thread(new Runnable() {
   
   
                    @Override
                    public void run() {
   
   
                        try {
   
   
                            Channel channel = RabbitMqUtils.getChannel();
                            System.out.println(Thread.currentThread()+"正在等待接收消息!!!!");
                            channel.basicConsume(RabbitMqUtils.QUEUE_NAME, true,(consumerTag, message) -> {
   
   
                                System.out.println("线程"+finalI +"消费者消费成功!!!"+consumerTag+"----------------消息为:"+new String(message.getBody()));
                            } , consumerTag -> {
   
   
                                System.out.println("线程"+finalI+"消费者取消消费或被中断!!!"+consumerTag);
                            });
                        } catch (Exception exception) {
   
   
                            exception.printStackTrace();
                        }

                    }
                });
        }
        for (int i = 0; i < 2; i++) {
   
   
            threads[i].start();
        }
    }
}

2.2 消息应答机制和消息重新入队

RabbitMQ引入了消息应答机制,Producer接收到消息并且处理该消息之后,告诉RabbitMQ已经处理了,RabbitMQ可以把该信息删除。

如果某个消费者消费一个消息A时,中间出现错误,而自动应答策略下,RabbitMQ传递消息后,立即就会将消息标记为删除。此时,消息A将丢失,Producer没有成功消费消息A。此时,RabbitMQ再次发送消息B给它,还会发生同样的状况,是很严重并且非常常见的消息队列问题。

自动应答

消息发送成功,立即认为传送成功。这种方式吞吐量较高,但是数据传输安全性低。当消息发送成功,但是Producer消费者连接或channel关闭或者其它原因没有接收消息。那么消息就丢失了。Producer也没有对消息数量限制,可能消息太多,造成消息积压,使内存耗尽,从而被操作系统杀死进程,此模式适合在Producer可以高效并以某种速率处理这些消息,并且对消息可靠性要求不高的情况下使用。

手动应答:类似TCP的三次握手,发ACK确认包。

  1. 肯定确认:Channel.basicAck()。RabbitMQ将知道该消息处理成功,可以将其丢弃。
  2. 否定应答:Channel.basicNack()。代表处理失败,不可以丢弃。
  3. 拒绝,驳回:Channel.basicReject()。代表不处理该消息了,可以将其丢弃。

basicNack()和basicReject()的区别是前者多一个参数Multiple,可以批量应答。basicAck()也可以批量应答。减少网络堵塞。批量应答的效果是针对channel信道的,整个channel信道的所有消息,都会批量的一次性进行应答。
在这里插入图片描述

  1. true:表示批量应答channel上未应答的消息,比如channel上有传送tag的消息5,6,7,8。当前处理的tag是8。那么此时如果basicAck()进行批量应答,5-8这些没有应答的消息,都会被确认收到消息应答。
  2. false:也就是不批量应答,当前处理tag是8,就只会应答8这个tag。5-7这三个消息依然不会被应答。

所以,建议,使用手动应答,并且不要使用批量应答,对每一个消息,单独进行处理。

消息自动重新入队

如果消息没有发送ACK确认,RabbitMQ将认为消息没有完全处理,会将其重新排队,其它消费者如果可以处理,将很快分发给另一个消费者。
在这里插入图片描述

2.3 消息应答机制改造

代码位置:
在这里插入图片描述
实现效果:线程0可以处理所有消息,需要1秒时间,线程1需要11秒时间,遇到消息test会处理失败,进行拒绝应答。线程1处理失败的消息,重新进入队列,让消费者进行处理。

  1. Producer代码:依次发送消息a,b,c,test,test,那么根据轮询,线程1处理时间会很长,它的channel中会堆积一些消息。
    在这里插入图片描述
import com.rabbitmq.client.Channel;
import com.yzpnb.rabbitmq.util.RabbitMqUtils;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

/**
 * 测试手动应答的生产者
 */
public class Producer {
   
   
    public static void main(String[] args) throws Exception {
   
   
        Channel channel = RabbitMqUtils.getChannel();
        channel.queueDeclare(RabbitMqUtils.QUEUE_NAME,true,false,false,null);
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        String msg = null;
        while((msg=bufferedReader.readLine())!=null){
   
   
            channel.basicPublish("",RabbitMqUtils.QUEUE_NAME,null,msg.getBytes(StandardCharsets.UTF_8));
            System.out.println("消息\""+msg+"\"发送完毕!!!");
        }
    }
}
  1. Consumer代码:实现尽管线程1处理消息失败,消息也不会丢失
    在这里插入图片描述
import com.rabbitmq.client.Channel;
import com.yzpnb.rabbitmq.util.RabbitMqUtils;

/**
 * 手动应答的Consumer
 */
public class ManualResponseConsumer {
   
   
    public static void main(String[] args) throws Exception {
   
   
        Thread[] threads = new Thread[2];
        for (int i = 0; i < 2; i++) {
   
   
            int finalI = i;
            threads[i]=
                    new Thread(new Runnable() {
   
   
                        @Override
                        public void run() {
   
   
                            try {
   
   
                                Channel channel = RabbitMqUtils.getChannel();

                                channel.basicConsume(RabbitMqUtils.QUEUE_NAME, false,(consumerTag, message) -> {
   
   
                                    try {
   
   
                                        System.out.println("线程"+finalI+"正在处理,需要处理:"+(finalI*10+1)+"秒");
                                        //第一个线程沉睡1秒,模拟处理时间较短的情况
                                        //第二个线程沉睡11秒,模拟处理时间较长的情况
                                        Thread.sleep(1000+1000*(finalI*10));
                                    } catch (InterruptedException e) {
   
   
                                        e.printStackTrace();
                    
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ydenergy_殷志鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值