03RabbitMQ工作队列

Work Queues 1 工作队列
工作队列

工作队列的主要思想是避免立即执行资源密集型任务。

一个生产者,一个消息队列,多个消费者,同样也称为点对点模式。

2 轮询分发消息
由于创建Channel的过程是重复的,所以单独抽出来放到工具类中。

public class RabbitMqUtil {
public static Channel getChanel() throws IOException, TimeoutException {
// 创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 为工厂绑定RabbitMQ的ip地址
factory.setHost(“192.168.121.138”);
// 为工厂绑定RabbitMQ的用户名称
factory.setUsername(“admin”);
// 为工厂绑定RabbitMQ的密码
factory.setPassword(“admin”);

    // 创建连接
    Connection connection = factory.newConnection();
    // 创建并返回一个信道
    return connection.createChannel();
}

}
重写写一个消费者,并且启动两个实例,模拟多个消费者

public class Consumer {
public static String QUEUE_NAME = “hello”;
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtil.getChanel();

    System.out.println("C2等待接收消息...");
    channel.basicConsume(QUEUE_NAME, true, (consumerTag, delivery) -> {<!-- -->
        System.out.println(new String(delivery.getBody()));
    }, (consumerTag) -> {<!-- -->
        System.out.println("接收消息被取消...");
    });
}

}
勾选“允许启动多个实例”

启动两次

再重写一个生产者

public class Producer {
public static String QUEUE_NAME = “hello”;

public static void main(String[] args) throws Exception {<!-- -->
    Channel channel = RabbitMqUtil.getChanel();

    channel.queueDeclare(QUEUE_NAME, false, false, false, null);
    Scanner read = new Scanner(System.in);
    while (read.hasNext()) {<!-- -->
        String message = read.next();
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        System.out.println("消息发送完成...");
    }
}

}
测试轮询

C1和C2,你一个我一个

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

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

自动应答
消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者channel关闭了,那么消息就会丢失。

也就是说,自动应答并不是很靠谱。

手动应答
Channel.basicAck(用于肯定确认):RabbitMQ知道该消息被接收并且成功处理了,可以将其丢弃了。
Channel.basicNack(用于否定确认):
Channel.basicReject(用于否定确认):与上面的相比少了一个参数,不处理该消息了,可以将其丢弃了。
Multiple的解释
手动应答的好处是可以批量应答并且减少网络拥堵。

true:代表批量应答channel上未应答的消息。
false:不使用批量应答==(实际开发中推荐不要使用批量应答)==
消息自动重新入队
如果消费者由于某些原因失去连接,导致消息未发送Ack确认,RabbitMQ将了解到消息未完全处理,并将对其重新进行排队。

手动应答演示
提前准备一个睡眠工具类

public class SleepUtil {
public static void sleep(int second){
try {
Thread.sleep(1000 * second);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
准备生产者

public class ProducerAck {
public static String QUEUE_NAME = “ack_queue”;

public static void main(String[] args) throws Exception {<!-- -->
    Channel channel = RabbitMqUtil.getChanel();

    channel.queueDeclare(QUEUE_NAME, false, false, false, null);
    Scanner read = new Scanner(System.in);
    System.out.println("请输入要发送的消息:");
    while (read.hasNext()) {<!-- -->
        String message = read.nextLine();
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
        System.out.println("生产者发出消息:" + message);
    }
}

}
消费者1

public class ConsumerAckC1 {
public static String QUEUE_NAME = “ack_queue”;

public static void main(String[] args) throws Exception {<!-- -->
    Channel channel = RabbitMqUtil.getChanel();
    channel.queueDeclare(QUEUE_NAME, false, false, false, null);
    System.out.println("C1等待接收消息处理时间较短...");

    DeliverCallback deliverCallback = (consumerTag, delivery) -> {<!-- -->
        String message = new String(delivery.getBody());
        // 沉睡1s
        SleepUtil.sleep(1);
        System.out.println("接收到消息..." + message);

        channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
    };

    boolean autoAck = false;
    channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, (consumerTag) -> {<!-- -->
        System.out.println(consumerTag+"消费者取消消费接口回调逻辑");
    });
}

}
消费者2

public class ConsumerAckC2 {
public static String QUEUE_NAME = “ack_queue”;

public static void main(String[] args) throws Exception {<!-- -->
    Channel channel = RabbitMqUtil.getChanel();
    channel.queueDeclare(QUEUE_NAME, false, false, false, null);
    System.out.println("C2等待接收消息处理时间较长...");

    DeliverCallback deliverCallback = (consumerTag, delivery) -> {<!-- -->
        String message = new String(delivery.getBody());

        // 沉睡10s
        SleepUtil.sleep(10);

        System.out.println("接收到消息..." + message);

         
        channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
    };

    channel.basicConsume(QUEUE_NAME, false, deliverCallback, (consumerTag) -> {<!-- -->
        System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
    });
}

}
演练过程
首先启动生产者,先不发消息
启动两个消费者
生产者先发送aa,再发送bb
由于轮询机制,消费者1会先收到aa,消费者睡眠时间较长,后收到bb
生产者再先后发送cc,dd
消费者1会先收到cc,然后在消费者2收到dd之前停止消费者2的程序
这时,消费者2收不到dd,消息dd重新入队,被消费者1收到(我尝试了几次,能成功,但是消费者1要等很久才能收到dd)
在发送者发送消息 dd,发出消息之后的把 C2 消费者停掉,按理说该 C2 来处理该消息,但是由于它处理时间较长,在还未处理完,也就是说 C2 还没有执行 ack 代码的时候,C2 被停掉了,此时会看到消息被 C1 接收到了,说明消息 dd 被重新入队,然后分配给能处理消息的 C1 处理了。

4 RabbitMQ持久化
保障当RabbitMQ服务停掉之后,消息生产者发送过来的消息不丢失。没有持久化的话,如果RabbitMQ重启,消息全部都会消失。

队列持久化
之前我们创建的队列都是非持久化的,rabbitmq 如果重启的化,该队列就会被删除掉,如果要队列实现持久化需要在声明队列的时候把 durable 参数设置为持久化。

注意:如果之前申明的同名队列不是持久化的,需要把原来的队列删除,不然会报错。

这时候,重启RabbitMQ,ack_queue会消失,而ack_queue2不会消失。但是!队列持久化并不代表该队列中的消息也能够持久化。重启后,虽然队列还会存在,可是里面的消息依然会消失

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

将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。**持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。**如果需要更强有力的持久化策略,参考后边课件发布确认章节。

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

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

预取值
本身消息的发送就是异步发送的,所以在任何时候,channel 上肯定不止只有一个消息另外来自消费者的手动确认本质上也是异步的。因此这里就存在一个未确认的消息缓冲区,因此希望开发人员能**限制此缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题。**这个时候就可以通过使用 basic.qos 方法设置“预取计数”值来完成的。该值定义通道上允许的未确认消息的最大数量。

prefetch就是缓冲区能容纳的消息数量,也就是可以放几个未处理的。

发布确认 1 概念
RabbitMQ上真正的持久化需要三个步骤:

队列持久化
数据持久化
发布确认
由于数据持久化必须保存到磁盘上才算是真正的持久化,但是有可能消息发送到队列中,队列还没来得及将数据保存到磁盘上就宕机了,即使做了前两步,消息也会丢失。所以需要一个发布确认,当消息保存在磁盘上之后,由MQ向生产者告知保存成功,这样才能非常肯定的说持久化了。

2 发布确认的策略 开启发布确认的方法
发布确认默认是没有开启的,需要手动调用confirmSelect方法,每当你想使用发布确认,都需要在 channel 上调用该方法。

单个确认发布
这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long)这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。

这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了。

public class ProducerConfirm {
public static final int MESSAGE_COUNT = 1000;

public static void main(String[] args) throws Exception {<!-- -->
    // 单个确认,耗时:1368ms
    publishMessageIndividually();
}
public static void publishMessageIndividually() throws IOException, TimeoutException, InterruptedException {<!-- -->
    Channel channel = RabbitMqUtil.getChanel();
    // 开启发布确认
    channel.confirmSelect();
    String queueName = UUID.randomUUID().toString();
    channel.queueDeclare(queueName, true, false, false, null);
    long begin = System.currentTimeMillis();

    for (int i = 0; i < MESSAGE_COUNT; i++) {<!-- -->
        channel.basicPublish("", queueName, MessageProperties.PERSISTENT_TEXT_PLAIN, (i + "").getBytes());
        // 发布确认
        boolean flag = channel.waitForConfirms();
        if (flag){<!-- -->
            System.out.println("消息发送成功" + i);
        }
    }
    long end = System.currentTimeMillis();
    System.out.println("发布" + MESSAGE_COUNT + "个单个确认消息,耗时:" + (end - begin) + "ms");

}

}
批量确认发布
上面那种方式非常慢,与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。

public static void publishMessageBatch() throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMqUtil.getChanel();
// 开启发布确认
channel.confirmSelect();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, true, false, false, null);
long begin = System.currentTimeMillis();
int confirmSize = 100;

for (int i = 0; i < MESSAGE_COUNT; i++) {<!-- -->
    channel.basicPublish("", queueName, MessageProperties.PERSISTENT_TEXT_PLAIN, (i + "").getBytes());
    if (i%confirmSize == 0){<!-- -->
        // 发布确认
        boolean flag = channel.waitForConfirms();
        if (flag){<!-- -->
            System.out.println("消息发送成功");
        }
    }
}
boolean flag = channel.waitForConfirms();
if (flag){<!-- -->
    System.out.println("消息最终确认,发送成功");
}

long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个批量确认消息,耗时:" + (end - begin) + "ms");

}
异步确认发布
异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功。

public static void publishMessageAsync() throws InterruptedException, IOException, TimeoutException {
Channel channel = RabbitMqUtil.getChanel();
// 开启发布确认
channel.confirmSelect();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, true, false, false, null);
long begin = System.currentTimeMillis();

// 成功回调
ConfirmCallback ackCallback = (var1, var3) -> {<!-- -->
    System.out.println("确认的消息:" + var1);
};
// 失败回调
ConfirmCallback nackCallback = (var1, var3) -> {<!-- -->
    System.out.println("未确认的消息:" + var1);
};
// 准备消息监听器,监听哪些成功了,哪些失败了
channel.addConfirmListener(ackCallback, nackCallback);


for (int i = 0; i < MESSAGE_COUNT; i++) {<!-- -->
    channel.basicPublish("", queueName, MessageProperties.PERSISTENT_TEXT_PLAIN, (i + "").getBytes());
}
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个异步确认消息,耗时:" + (end - begin) + "ms");

}
由于发送的太快了,只会返回成功接收的最大的编号(发送速度太快了,监听器跟不上?)

如何处理异步未确认消息
最好的解决的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,比如说用 ConcurrentLinkedQueue 这个队列在 confirm callbacks 与发布线程之间进行消息的传递。

文章转自:03RabbitMQ工作队列_Java-答学网

作者:答学网,转载请注明原文链接:http://www.dxzl8.com/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值