目录
1.Hello World
1. 依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ys.rabbitmq</groupId>
<artifactId>rabbitmq-hello</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!--rabbitmq 依赖客户端-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.8.0</version>
</dependency>
<!--操作文件流的一个依赖-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
2. 生产者代码
package com.ys.rabbitmq.one;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 生产者:发送消息
*
*
*/
public class Producer
{
//队列名称
public static final String QUEUE_NAME="HELLO";
//发消息
public static void main(String[] args) throws Exception{
//创建一个连接工厂
ConnectionFactory factory=new ConnectionFactory();
//工厂IP 连接RabbitMQ的队列
factory.setHost("47.106.78.230");
factory.setUsername("guest");
factory.setPassword("guest");
//创建连接
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是因为浏览器访问的端口是15672。
但是用java程序连接,端口就变成了5672。
所有需要在linux中,将5672端口开放
发送成功!
3.消费者代码
package com.ys.rabbitmq.one;
import com.rabbitmq.client.*;
/**
* 消费者 接收消息的
*/
public class Consumer
{
//队列名称
public static final String QUEUE_NAME="HELLO";
//发消息
public static void main(String[] args) throws Exception{
//创建一个连接工厂
ConnectionFactory factory=new ConnectionFactory();
//工厂IP 连接RabbitMQ的队列
factory.setHost("47.106.78.230");
factory.setUsername("guest");
factory.setPassword("guest");
//创建连接
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);
}
}
接收成功
2. Work Queues
工作队列(又称任务队列)的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。相反我们安排任务在之后执行。我们把任务封装为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当有多个工作线程时,这些工作线程将一起处理这些任务。
2.1 启动两个接收线程
Untils
package com.ys.rabbitmq.utils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* 创建信道的工具类
*/
public class RabbitMqUtils
{
//得到一个链接的Chanel
public static Channel getChannel() throws Exception
{
//创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("47.106.78.230");
factory.setUsername("guest");
factory.setPassword("guest");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
return channel;
}
}
Work01接收
package com.ys.rabbitmq.two;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.ys.rabbitmq.utils.RabbitMqUtils;
/**
* 这是一个工作线程相当于一个消费者
*/
public class Work01
{
//队列名称
public static final String QUEUE_NAME="HELLO";
//接收消息
public static void main(String[] args) throws Exception {
Channel channel=RabbitMqUtils.getChannel();
DeliverCallback deliverCallback=(consumerTag,delivery)->{
String receivedMessage = new String(delivery.getBody());
System.out.println("接收到消息:"+receivedMessage);
};
CancelCallback cancelCallback=(consumerTag)-> {
System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
};
System.out.println("C2 消费者启动等待消费......");
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
我们通过 设置idea 设置允许并行运行,即我们多次修改c1 c2并运行 启动两次即实现多线程运行
2.2 发送消息
package com.ys.rabbitmq.two;
import com.rabbitmq.client.Channel;
import com.ys.rabbitmq.utils.RabbitMqUtils;
import java.util.Scanner;
public class Task01
{
//队列名称
public static final String QUEUE_NAME="HELLO";
public static void main(String[] args) throws Exception {
Channel channel= RabbitMqUtils.getChannel();
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化 默认消息存储在内存中
* 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
* 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
* 5.其他参数
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
Scanner scanner=new Scanner(System.in);
while (scanner.hasNext())
{
String message=scanner.next();
/**
* 发送一个消息
* 1.发送到那个交换机
* 2.路由的 key 是哪个 本次是队列名称
* 3.其他的参数信息
* 4.发送消息的消息体
*/
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("发送消息:"+message+" 完成");
}
}
}
2.3 结果
C1
C2
他们是通过轮训的方式 实现消费
3. 消息应答
3.1 基本概念
消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成了部分突然它挂掉了,会发生什么情况。RabbitMQ 一旦向消费者传递了一条消息,便立即将该消息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。以及后续发送给该消费这的消息,因为它无法接收到。
消息应答就是:消费者在接收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了。
1. 自动应答
消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权
衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者 channel 关闭,那么消息就丢
失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制,
当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终
使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅适用在消费者可以高效并
以某种速率能够处理这些消息的情况下使用。
2. 消息应答的方法
A.Channel.basicAck(用于肯定确认)
RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了
B.Channel.basicNack(用于否定确认)
C.Channel.basicReject(用于否定确认)
与 Channel.basicNack 相比少一个参数
不处理该消息了直接拒绝,可以将其丢弃了
3. Multiple 的解释
如果选择false就相当于是累计确认
4. 消息自动重新入队
如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或 TCP 连接丢失),导致消息未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。
3.2 生产者
package com.ys.rabbitmq.three;
import com.rabbitmq.client.Channel;
import com.ys.rabbitmq.utils.RabbitMqUtils;
import java.util.Scanner;
/**
* 消息在手动应答时是不丢失、放回队列中重新消费
*/
public class Task2
{
private static final String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] argv) throws Exception {
try (Channel channel = RabbitMqUtils.getChannel())
{
channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
Scanner sc = new Scanner(System.in);
System.out.println("请输入信息");
while (sc.hasNext()) {
String message = sc.nextLine();
channel.basicPublish("", TASK_QUEUE_NAME, null, message.getBytes("UTF-8"));
System.out.println("生产者发出消息" + message);
}
}
}
}
3.3 消费者
SleepUtils
package com.ys.rabbitmq.utils;
public class SleepUtils {
public static void sleep(int second){
try {
Thread.sleep(1000*second);
} catch (InterruptedException _ignored) {
Thread.currentThread().interrupt();
}
}
}
Work03
package com.ys.rabbitmq.three;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.ys.rabbitmq.utils.RabbitMqUtils;
import com.ys.rabbitmq.utils.SleepUtils;
public class Work03
{
private static final String ACK_QUEUE_NAME="ack_queue";
public static void main(String[] args) throws Exception {
Channel channel= RabbitMqUtils.getChannel();
System.out.println("Work03等待处理消息时间短");
//消息消费的时候如何处理消息
DeliverCallback deliverCallback=(consumerTag, delivery)->
{
String message= new String(delivery.getBody());
SleepUtils.sleep(1);
System.out.println("接收到消息:"+message);
/**
* 1.消息标记 tag
* 2.是否批量应答未应答消息
*/
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
};
//采用手动应答
boolean autoAck=false;
channel.basicConsume(ACK_QUEUE_NAME,autoAck,deliverCallback,(consumerTag)->{
System.out.println(consumerTag+"消费者取消消费接口回调逻辑");
});
}
}
Work04
package com.ys.rabbitmq.three;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.ys.rabbitmq.utils.RabbitMqUtils;
import com.ys.rabbitmq.utils.SleepUtils;
public class Work04
{
private static final String ACK_QUEUE_NAME="ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
System.out.println("C2 等待接收消息处理时间较长");
//消息消费的时候如何处理消息
DeliverCallback deliverCallback=(consumerTag, delivery)->{
String message= new String(delivery.getBody());
SleepUtils.sleep(10);
System.out.println("接收到消息:"+message);
/**
* 1.消息标记 tag
* 2.是否批量应答未应答消息
*/
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
};
//采用手动应答
boolean autoAck=false;
channel.basicConsume(ACK_QUEUE_NAME,autoAck,deliverCallback,(consumerTag)->{
System.out.println(consumerTag+"消费者取消消费接口回调逻辑");
});
}
}
可以看到两个处理消息的时间不同,消费者也同样是轮训的方式一次接收
现在我将发送消息 e 如果正常的话是由04接收,但是我这个时候模拟宕机即停止04的运行,可以发现,消息被03消费
4. RabbitMQ 持久化
默认情况下 RabbitMQ 退出或由于某种原因崩溃时,它忽视队列和消息,除非告知它不要这样做。确保消息不会丢失需要做两件事:我们需要将队列和消息都标 记为持久化。
4.1 队列持久化
queueDeclare在队列声明函数中 设置durable为true
4.2 消息持久化
要想让消息实现持久化需要在消息生产者修改代码,
MessageProperties.PERSISTENT_TEXT_PLAIN 添加这个属性。
将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了
4.3 不公平分发
在最开始的时候我们学习到 RabbitMQ 分发消息采用的轮训分发,但是在某种场景下这种策略并不是
很好,比方说有两个消费者在处理任务,其中有个消费者 1 处理任务的速度非常快,而另外一个消费者 2处理速度却很慢,这个时候我们还是采用轮训分发的化就会到这处理速度快的这个消费者很大一部分时间处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但是RabbitMQ 并不知道这种情况它依然很公平的进行分发。为了避免这种情况,我们可以设置参数 channel.basicQos(1);
这个是 消费者 在接收消息之前设置的!!
4.4 预取值
能限制此缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题,以通过使用 basic.qos 方法设置“预取计数”值来完成的。该值定义通道上允许的未确认消息的最大数量。一旦数量达到配置的数量,RabbitMQ 将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认,
channel.basicQos(5);
消费者在接收消息之前设置
5. 发布确认
5.1 基本概念
也就是说如果你保存到磁盘上了,你要返回确认信息
5.2 单个确认
这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布 ,** waitForConfirmsOrDie(long) ** 这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了。
package com.ys.rabbitmq.four;
import com.rabbitmq.client.Channel;
import com.ys.rabbitmq.utils.RabbitMqUtils;
import java.util.UUID;
public class ConfirmMessage
{
public static final int MESSAGE_COUNT=1000;
public static void main(String[] args) throws Exception {
//1.单个确认
publishMessageIndividually();
}
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("",message,null,message.getBytes());
//发送完就马上发布确认
boolean flag = channel.waitForConfirms();
if(flag){
System.out.println("消息发送成功");
}
}
long end=System.currentTimeMillis();
System.out.println("发送了"+MESSAGE_COUNT+"消息,用时:"+(end-begin)+"ms");
}
}
5.3 批量确认
与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地
提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。
//批量
public static void publicMessageBatch() throws Exception
{
Channel channel= RabbitMqUtils.getChannel();
//声明队列
String queueName= UUID.randomUUID().toString();
channel.queueDeclare(queueName,true,false,false,null);
//开启发布确认
channel.confirmSelect();
//开始时间
long begin=System.currentTimeMillis();
//批量确认的消息大小
int batchSize=100;
//批量发送消息
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message=i+"";
channel.basicPublish("",message,null,message.getBytes());
//发送完就马上发布确认
if(i%batchSize==0)
{
boolean flag = channel.waitForConfirms();
if(flag){
System.out.println("消息发送成功");
}
}
}
long end=System.currentTimeMillis();
System.out.println("发送了"+MESSAGE_COUNT+"消息,用时:"+(end-begin)+"ms");
}
5.4 异步发布确认
异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功
//异步发布
public static void publicMessageAsync() throws Exception
{
Channel channel= RabbitMqUtils.getChannel();
//声明队列
String queueName= UUID.randomUUID().toString();
channel.queueDeclare(queueName,true,false,false,null);
//开启发布确认
channel.confirmSelect();
//开始时间
long begin=System.currentTimeMillis();
//准备消息监听器 监听消息发送成功or失败
//消息成功回调函数
ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {
System.out.println("已确认的消息:"+sequenceNumber);
};
//消息失败回调函数
/**
*
* 1.消息序列号
* 2.是否批量确认
*/
ConfirmCallback nackCallback = (sequenceNumber, multiple) -> {
System.out.println("未确认消息:"+sequenceNumber);
};
channel.addConfirmListener(ackCallback,nackCallback);
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message=i+"";
channel.basicPublish("",message,null,message.getBytes());
}
long end=System.currentTimeMillis();
System.out.println("异步发送了"+MESSAGE_COUNT+"消息,用时:"+(end-begin)+"ms");
}
5.5 异步发布确认之处理未确认消息
最好的解决的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,比如说用 ConcurrentSkipListMap这个在 confirm callbacks 与发布线程之间进行消息的传递。
//异步发布
public static void publicMessageAsync() throws Exception
{
Channel channel= RabbitMqUtils.getChannel();
//声明队列
String queueName= UUID.randomUUID().toString();
channel.queueDeclare(queueName,true,false,false,null);
//开启发布确认
channel.confirmSelect();
/**
* 线程安全有序的一个跳表,适用于高并发的情况
* 1.轻松的将序号与消息进行关联
* 2.轻松批量删除条目 只要给到序列号
* 3.支持并发访问
*/
ConcurrentSkipListMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
//开始时间
long begin=System.currentTimeMillis();
//准备消息监听器 监听消息发送成功or失败
//消息成功回调函数
ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {
//2.删除掉已经确认的消息,那么剩下的就是未确认的消息
if(multiple)
{
ConcurrentNavigableMap<Long, String> confirmedMap = outstandingConfirms.headMap(sequenceNumber);
confirmedMap.clear();
}else{
outstandingConfirms.remove(sequenceNumber);
}
System.out.println("已确认的消息:"+sequenceNumber);
};
//消息失败回调函数
/**
*
* 1.消息序列号
* 2.是否批量确认
*/
ConfirmCallback nackCallback = (sequenceNumber, multiple) -> {
//
System.out.println("未确认消息:"+outstandingConfirms.get(sequenceNumber));
};
channel.addConfirmListener(ackCallback,nackCallback);
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message=i+"";
//1.记录下所有发送的消息
outstandingConfirms.put(channel.getNextPublishSeqNo(),message);
channel.basicPublish("",message,null,message.getBytes());
}
long end=System.currentTimeMillis();
System.out.println("异步发送了"+MESSAGE_COUNT+"消息,用时:"+(end-begin)+"ms");
}