Rabbitmq学习笔记

RabbitMQ

1.1概念

Rabbitmq是一个消息中间件,他接受并转发消息,你可以把他当做一个快递站点,当你需要发送一个包裹时,你把包裹放到快递站,快递员最后会把你的快递送到收件人那里,按照这种逻辑Rabbitmq是一个快递站,它不处理而是接受,存储和转发消息数据。

1.2四大概念

  • 生产者
  • 交换机
  • 队列
  • 消费者

Rabbitmq学习笔记_第1张图片

1.3核心部分

  • 简单模式
  • 工作模式
  • 发布订阅模式
  • 路由模式
  • 主题模式
  • 发布确认模式

Rabbitmq学习笔记_第2张图片

Broker就是Rabbitmq的实体,表示接受和分发消息的应用,也叫Rabbitmq的服务器

2.1生产者

public class Producer {
    //队列名称
    public static final String QUEUE_NAME = "hello";
    //发消息
    public static void main(String[] args) throws IOException, TimeoutException {
        //创建一个链接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //工厂IP连接rabbitmq的队列,这里我配置的本地端口
        factory.setHost("127.0.0.1");
        //用户名和密码
        factory.setUsername("guest");
        factory.setPassword("guest");
        //创建链接
        Connection connection = factory.newConnection();
        //通过链接获取信道
        Channel channel = connection.createChannel();
        //声明一个队列
        //第一个参数:队列名称
        //第二个参数:队列中消息是否持久化,默认情况(false)消息存储在内存中,持久化就是在磁盘上
        //第三个参数:该队列是否只供一个消费者消费,是否进行消息共享,如果是true就可以多个消费者消费
        //第四个参数:是否自动删除,最后一个消费之断开连接后,该队列是否自动删除,true自动删除,false不自动删除
        //第五个参数:其他参数
        channel.queueDeclare(QUEUE_NAME,true,false,false,null);
        String message = "hello world";
        //发消息用信道发
        //发送一个消费
        //第一个参数:发送到哪个交换机
        //第二个参数:路由的key值是那个。本次是队列名称
        //第三个参数:其他参数信息
        //第四个参数:发送消息的消息体,需要将消息转化为二进制的形式进行传输在信道上
        channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
        System.out.println("消息发送完毕");
    }
}

2.2消费者

public class Consumer {
    //队列的名称,接收队列的消息
    public static final String QUEUE_NAME = "hello";
    //接收消息
    public static void main(String[] args) throws IOException, TimeoutException {
        //创建链接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("127.0.0.1");
        connectionFactory.setUsername("guest");
        connectionFactory.setUsername("guest");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        //声明
//        DeliverCallback deliverCallback = new DeliverCallback() {
//            @Override
//            public void handle(String s, Delivery delivery) throws IOException {
//
//            }
//        }
        //第二个参数就是消息
        DeliverCallback deliverCallback = (s, delivery) -> {
            System.out.println(new String(delivery.getBody()));
        };

        CancelCallback cancelCallback = s -> {
            System.out.println("消息消费被中断");
        };


        //消费者消费消息
        //第一个参数:消费哪个队列
        //第二个参数:消费完成后是否要自动应答,true代表自动应答,false代表手动应答
        //第三个参数:消费者未成功消费的回调
        //第四个参数:消费者取消消费回调
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
    }
}

3.1轮询分发消息

工作队列的主要思想就是避免立即执行资源密集型任务,而不得不等待他完成,相反我们在安排任务在之后执行。我们把任务封装为消息并将其发送到队列,在后台运行的工作进程将任务弹出并最终执行作业,当有多个工作线程的时候,这些工作线程将一起处理这些任务

3.1.1抽取工具类

public class RabbitMqUtils {
    public static Channel getChannel() throws Exception{
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("127.0.0.1");
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        return channel;
    }
}

3.2消息应答

消费者完成一个任务可能要一段时间,如果其中一个消费者处理一个长的任务并且只完成部分他就突然挂掉了,会发生什么情况,MQ一旦向消费者传递了一条消息,便立即将该消息标记为删除,在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息,以及后续发送给该消费者的消息,因为它无法接收到。

为了保证消息在发送过程中不丢失,rabbitmq引入消息应答机制,消息应答就是:消费者在接受消息并处理该消息后,告诉rabbitmq它已经处理了,rabbitmq可以把消息删除了。

3.1.1自动应答

消息发送之后即被认为已经传输成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者channe关闭,那么数据就丢失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制,当然这样有可能使得消费者这边由于接受过多还来不及处理的消息,导致消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死,这种模式仅适用于在消费者可以高效以某种速率可以处理这些消息的情况下使用。

3.1.2消息应答的方法

Rabbitmq学习笔记_第3张图片

3.1.3消息自动重新入队

如果消费者因为某些原因失去连接,其通道已关闭,连接已关闭或者TCP连接丢失,导致消息未发送ACK确认,Rabbitmq将了解到消息未完全处理,将对其重新排队,如果消费者可以处理,它将很快将其重新分发给另外一个消费者,这样,即使某个消费者偶尔死亡,也可以确认不会丢失任何消息。

3.2RabbitMq持久化

默认情况下Rabbitmq退出或者由于某种原因崩溃时,他忽视队列和消息,除非告知他不要这么做,确保消息不会丢失需要做到两件事:我们需要将队列和消息都标记为持久化。

3.2.1队列如何实现持久化

image-20220926155313454

即使这时重启也会被持久化

3.2.2消息持久化

Rabbitmq学习笔记_第4张图片

将消息标记为持久化并不能完全保证不会丢失信息,尽管他告诉Rabbitmq将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候,但是还没有存完,消息还在缓存的一个间隔点,此时没有真正的写入硬盘,持久性保证不强,但是对于我们简单队列来说,已经绰绰有余了。

3.3.1不公平分发

因为Rabbitmq分发消息是以轮询的方式分发,但是在某种场景下这种策略并不是很好,比方说有两个消费者在处理任务,其中有一个消费者1处理速度非常快,而另一个消费者2处于空闲状态,而处理慢的那个消费者始终在干活,这种分配方式在这种情况下其实就不太好,但是Rabbitmq并不知道这种情况他依然很公平的进行分发。

这种情况下,我们可以设置参数channel.basicQos(1)

3.4.1预取值

Rabbitmq学习笔记_第5张图片

提前设置在信道上,channel.basicQos(2)

4.1发布确认

Rabbitmq学习笔记_第6张图片

4.1.1开启发布确认方法

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

4.1.2单个发布确认

发一条我必须确认一条,是一种同步的形式。发布速度特别慢,需要等待他人确认。

public static void publishMessageIndividually() throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        //队列的声明
        String queueName = UUID.randomUUID().toString();
        channel.queueDeclare(queueName,true,false,false,null);
        //开启发布确认
        channel.confirmSelect();
        //开始时间
        long begin = System.currentTimeMillis();
        //批量发消息 单个确认
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            String message = i+"";
            channel.basicPublish("",queueName,null,message.getBytes());
            //单个消息马上发布确认
            boolean b = channel.waitForConfirms();
            //标记如果为true就是发送成功了
            if (b){
                System.out.println("消息发布成功了");
            }
        }
        //结束时间
        long end = System.currentTimeMillis();
        System.out.println("发布"+MESSAGE_COUNT+"条单独确认消息,耗时:"+(end-begin)+"毫秒");

    }

4.1.3批量发布确认

这样速度快很多,但是我们不确定在批量发布确认的时候是否会在中间出现消息丢失的情况。

public static void publishMessageBatch() throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        //队列的声明
        String queueName = UUID.randomUUID().toString();
        channel.queueDeclare(queueName,true,false,false,null);
        //开启发布确认
        channel.confirmSelect();
        //开始时间
        long begin = System.currentTimeMillis();
        //批量发消息 批量确认 100条100条确认
        int batchSize = 100;
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            String message = i+"";
            channel.basicPublish("",queueName,null,message.getBytes());
            //批量确认
            if (i%batchSize==0){
                channel.waitForConfirms();
            }
        }
        //结束时间
        long end = System.currentTimeMillis();
        System.out.println("发布"+MESSAGE_COUNT+"条批量确认消息,耗时:"+(end-begin)+"毫秒");
    }

4.1.4异步消息发布确认

Rabbitmq学习笔记_第7张图片

异步确认虽然逻辑要比前两个复杂,但是性价比很高,无论是可靠性还是效率都比前两个好,他是利用回调函数来达到消息可靠性传输的,这个中间件也是通过函数回调来保证是否成功,我们生产者只需要不断的发就可以了,是否收到由消息队列实体broker来确认。

public static void publishMessageAsync() throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        //队列的声明
        String queueName = UUID.randomUUID().toString();
        channel.queueDeclare(queueName,true,false,false,null);
        //开启发布确认
        channel.confirmSelect();

        //线程安全有序的一个哈希表,适用于高并发情况下
        //轻松的将序号和消息进行关联
        //轻松删除条目,只需要获得序号
        //支持高并发,多线程
        ConcurrentSkipListMap<Long,String> outstandingConfirms = new ConcurrentSkipListMap<>();


        //监听器所需要的接口
        //消息成功回调函数
        ConfirmCallback ackCallback = (l, b) -> {
            //删除确认的消息
            if (b){
                ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(l);
                confirmed.clear();
            }else {
                outstandingConfirms.remove(l);
            }
            System.out.println("确认的消息:"+l);
        };
        //消息失败回调函数
        ConfirmCallback nackCallback = (l, b) -> {
            System.out.println("未确认的消息:"+l);
        };

        //准备消息的监听器,那些成功了,哪些失败了
        channel.addConfirmListener(ackCallback,nackCallback);
        //异步

        //批量发布消息1000条全发完,确认的事情不用管
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            String message = "消息"+i;
            channel.basicPublish("",queueName,null,message.getBytes());
            //记录所有要发送的消息
            outstandingConfirms.put(channel.getNextPublishSeqNo(),message);
        }
    }

5.1交换机

在之前,我们创建一个工作队列,我们假设的是工作队列背后,每个任务都恰好交付给一个消费者(工作进程),在这一部分中,我们将做一些完全不同的事情,我们将消息传达给多个消费者。这种模式叫做发布/订阅。

Rabbitmq学习笔记_第8张图片

5.1.1Exchanges概念

Rabbitmq消息传递模型的核心思想是:生产者的生产的消息不会直接发送到队列上,实际上,通常生产者自己都不知道这些消息传递到了哪些队列当中

相反,生产者只能将消息发送到交换机,交换机的工作内容很简单,一方面它来接受生产者的消息,一方面将它们推入队列,交换机必须确切知道如何处理收到的消息,是应该把这些消息放入特定队列还是把他们放入许多队列或者是丢弃他们,这就得由交换机来决定。

5.1.2Exchanges类型

  • 直接direct
  • 主题topic
  • 标题headers
  • 扇出fanout

5.2临时队列

之前我们学习的使用特定名称的队列,ack和ack_queue。

每当我们连接到rabbitmq的时候,我们都需要一个全新的空队列,为此我们可以创建一个具有随机名称的队列,或者能让服务器为我们选择一个随机队列的名称就更好了,其次一旦我们断开了消费者的链接,队列将自动删除

创建临时队列

String queueName = channel.queueDeclare().getQueue();

5.3Fanout

这种类型很简单,正如名称中那样,他是将接受到的消息广播到它知道的所有队列当中,系统中默认有些exchange类型

Rabbitmq学习笔记_第9张图片

发送代码

public class EmitLog {
    public static final String EXCHANGE_NAME = "logs";
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
        Scanner scanner = new Scanner(System.in);
        while(scanner.hasNext()){
            String next = scanner.next();
            channel.basicPublish(EXCHANGE_NAME,"",null,next.getBytes("UTF-8"));
            System.out.println("生产者发出消息"+next);
        }
    }
}

接收代码

public class ReceiveLogs01 {
    public static final String EXCHANGE_NAME="logs";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        //声明一个交换机
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
        //声明一个临时队列
        //生成一个临时队列,队列的名称是随机的
        //当消费者断开与队列的链接的时候,队列就自动删除
        String queue = channel.queueDeclare().getQueue();
        //交换机绑定队列
        channel.queueBind(queue,EXCHANGE_NAME,"");
        System.out.println("等待接受消息,把消息打印出来。。。。");
        //接收消息接口
        DeliverCallback deliverCallback = (s, delivery) -> {
            System.out.println("01接收消息:"+new String(delivery.getBody(),"UTF-8"));
        };
        //消费者取消消息时回调接口
        CancelCallback cancelCallback = s -> {
        };

        channel.basicConsume(queue,true,deliverCallback,cancelCallback);
    }
}

5.4Direct Exchange(路由模式)

路由模式发布消息代码

public class DirectLogs {
    public static final String EXCHANGE_NAME = "direct_logs";
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            String next = scanner.next();
            channel.basicPublish(EXCHANGE_NAME,"info",null,next.getBytes("UTF-8"));
            System.out.println("生产者发出消息"+next);
        }
    }
}

在这里,RoutingKey成为重点,生产者需要指定消息接收队列的路由,将消息准确的发送到某一个队列中去

5.5Topics(主题模式)

发送类型是topic交换机的消息routing_key不能随意写,必须满足一定的条件,**他必须是一个单词列表,以点号分开。**这些单词可以是任意单词。

  • *可以代替一个单词
  • #可以代替0个或多个单词

Rabbitmq学习笔记_第10张图片

6.1死信队列

死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer将消息投递到broker或者直接到Queue当中了,consumer从queue中取出消息进行消费,但某些时候,由于特定的原因导致queue中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列

应用场景:为了保证订单业务的消息数据不丢失,需要使用到Rabbitmq的死信队列机制,当消息消费产生异常的时候,将消息投入死信队列当中,还比如说:用户在商城下单成功并点击去支付后在指定时间内未支付自动失效。

6.2死信的来源

  • 消息TTL过期
  • 队列达到最大长度
  • 消息被拒绝

Rabbitmq学习笔记_第11张图片

6.3死信队列代码演示

消费者

public class Consumer01 {
    //普通交换机名称
    public static final String NORMAL_EXCHANGE = "normal_exchange";
    //死信交换机名称
    public static final String DEAD_EXCHANGE = "dead_exchange";
    //普通队列名称
    public static final String NORMAL_QUEUE = "normal_queue";
    //死信队列名称
    public 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);
        //声明普通队列
        HashMap<String, Object> arguments = new HashMap<>();
        //设置过期时间
        arguments.put("x-message-ttl", 10000);
        //正常队列设置死信交换机
        arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE);
        //设置死信routingkey
        arguments.put("x-dead-letter-routing-key", "lisi");
        //绑定队列和交换机
        channel.queueDeclare(NORMAL_QUEUE, false, false, false, arguments);
        channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "zhangsan");
        //声明死信队列
        channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
        //绑定死信队列和交换机
        channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "lisi");
        System.out.println("等待接受消息。。。。。");

        DeliverCallback deliverCallback = (s, delivery) -> {
            System.out.println("01接收的消息是" + new String(delivery.getBody(), "UTF-8"));
        };
        CancelCallback cancelCallback = s -> {

        };
        channel.basicConsume(NORMAL_QUEUE, true, deliverCallback, cancelCallback);
    }
}

在这个过程中,我们主要需要绑定普通队列和死信队列的关系,普通交换机和死信交换机的关系,使他们俩产生联系

死信队列生产者代码:

public class Producer {
    public static final String NORMAL_EXCHANGE = "normal_exchange";
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
        //死信消息 设置ttl时间
        for (int i = 0; i < 10; i++) {
            String message = "info"+i;
            channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",properties,message.getBytes("UTF-8"));
        }
    }
}

在这里设置ttl时间,使我们发送一条消息需要10s,让普通队列消费者假死,然后所有的消息就会进到死信队列当中

随后我们编写死信队列消费者代码,将死信队列消费

public class Consumer02 {
    public static final String DEAD_QUEUE = "dead_queue";
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("等待接受消息。。。。。");

        DeliverCallback deliverCallback = (s, delivery) -> {
            System.out.println("02接收的消息是" + new String(delivery.getBody(), "UTF-8"));
        };
        CancelCallback cancelCallback = s -> {

        };
        channel.basicConsume(DEAD_QUEUE, true, deliverCallback, cancelCallback);
    }
}

设置普通队列长度,超过长度的消息会自动进入死信队列

//设置正常队列的长度限制
arguments.put("x-max-length",6);

在消息接收逻辑中设置拒绝策略

Rabbitmq学习笔记_第12张图片

当消息为info5的时候,拒绝消费,并且不放回普通队列当中,在此需要开启手动应答,否则操作失效

7.1延迟队列

  • 订单在十分钟内未支付则自动取消
  • 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒
  • 用户创建成功后,如果三天内没有登录则进行短信提醒
  • 用户发起退款,如果三天内没有得到处理则会通知相关运营人员
  • 预定会议后,需要在预定时间点前十分钟通知各会议人员参加会议

这些场景都有一个特点,需要在某个事件发生之后或者之前指定时间完成某项任务,可以使用定时任务,一直轮询数据,几秒钟查一次,取出需要被处理的数据,处理就可以了,如果数据量比较少,确实可以这样做,比如:对于账单一周之内未支付则进行自动结算这样的需求,如果对于时间不是很严格,而是宽松的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单,确实也是一个可行的方案,但是对于数据量比较大的,并且时效性比较强的场景,比如:订单十分钟内未支付则关闭,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单的检查,同时会对数据库带来很大压力,无法满足业务要求且性能低下

7.2整合springboot

你可能感兴趣的:(mq,java-rabbitmq,rabbitmq,学习)