目录
1、MQ简介
1.1、基本概念
MQ,即消息队列。本质上就是一个队列,遵循先进先出原则。MQ是一种跨进程的通信机制,用于传递上下游不同进程之间的消息。在互联网架构中MQ主要用于逻辑解耦和物理解耦。使用了MQ之后,上下游系统没有直接的依赖关系,都是通过中间件MQ来完成的。
所谓跨进程通信是指不同的进程之间进行通信。进程间通信方式有信号、管道、消息队列、共享内存。消息队列是一种可跨进程异步的进程通信方式。
有关进程通信的了解可以参考:进程通信概念和进程通信方式_xy913741894的博客-CSDN博客
1.2、应用场景
1.2.1、流量消峰
订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的所以只能限制订单超过一万后不允许用户下单。但是如果使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分 散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验要好。
1.2.2、应用解耦
以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性。
1.2.3、异步处理
有些服务间的调用是异步的,比如服务A调用服务B,由于B完成需要一段时间,为了让A不必花费时间等待B完成。在以前有两种解决方法:一、让服务A轮询每隔一段时间去调一下B的API,看B是否已经完成。二、让B在执行完成后调一个callback api去通知A。但是这两种方式都会额外增加系统开销。因此在A和B之间引入MQ,通过消息总线,只需监听B处理完成的消息。当B处理完成后会发一条消息给MQ,MQ会将此消息发给A。这样 A服务既不用循环调用 B 的查询 api,也不用提供 callback api。同样B服务也不用做这些操作。A服务还能及时的得到异步处理成功的消息。
![](https://i-blog.csdnimg.cn/blog_migrate/583484354a35ffef33a7f6e67ecbffaf.png)
1.3、RabbitMQ四大核心概念
1.3.1、生产者
1.3.2、交换机
2、实现一个RabbitMQ
1、rabbitMQ客户端依赖
<!--rabbitmq 依赖客户端-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.8.0</version>
</dependency>
package rabbitMQ.util;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author: wu linchun
* @time: 2021/7/8 10:57
* @description:
*/
public class RabbitMqUtils {
/**
* @author: wu linchun
* @create: 2021/7/8 11:48
* @desc: 得到一个连接的 channel
**/
public static Channel getChannel() throws IOException, TimeoutException {
//创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.43.171");
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
return channel;
}
}
3、消息生产者
package rabbitMQ.task;
import com.rabbitmq.client.Channel;
import rabbitMQ.util.RabbitMqUtils;
import java.util.Scanner;
/**
* @author: wu linchun
* @time: 2021/7/8 11:54
* @description: 消息生产者
*/
public class MessageProductor{
private static final String QUEUE_NAME = "ack_001";
public static void main(String[] args) throws Exception {
try (Channel channel = (Channel) RabbitMqUtils.getChannel();) {
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化 默认消息存储在内存中(true表示持久化,数据是存在磁盘中的。false表示非持久化,数据是存在内存中的)
* 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
* 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
* 5.其他参数
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//从控制台当中接受信息
Scanner input = new Scanner(System.in);
while (!input.hasNext("#")) {
String message = input.next();
/**
* 发送一个消息
* 1.发送到那个交换机
* 2.路由的 key 是哪个
* 3.其他的参数信息
* 4.发送消息的消息体
*/
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("发送消息完成:" + message);
}
}
}
}
4、消息消费者
为了确保消息不会丢失,RabbitMQ支持消息应答。消费者发送一个消息应答,告诉RabbitMQ这个消息已经接收并且处理完毕了,RabbitMQ就可以删除它了。
如果一个消费者挂掉却没有发送应答,RabbitMQ会理解为这个消息没有处理完全,然后交给另一个消费者去重新处理。这样,你就可以确认即使消费者偶尔挂掉也不会丢失任何消息了。
手动应答
package rabbitMQ.work;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import rabbitMQ.util.RabbitMqUtils;
import java.util.Map;
/**
* @author: wu linchun
* @time: 2021/7/8 20:31
* @description: 消费者02
*/
public class Worker02 {
private static final String QUEUE_NAME = "ack_001";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
//消息回调,接收消息
DeliverCallback deliverCallback = (consumerTag, message) -> {
//沉睡10s,模拟其他代码执行
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String receivedMessage = new String(message.getBody(), "UTF-8");
System.out.println("worker02接收到消息:" + receivedMessage);
//手动应答
/**
* 1、消息标记 tag
* 2、是否批量应答 false:不批量应答信道中的消息 true:批量应答信道中的消息
* 由于批量应答可能会导致消息丢失,所以不批量应答,处理一个应答一个
* 消息在手动应答是不会丢失的,一旦原本接收消息的消费者挂掉后,该消息会被其他消费者应答
*/
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
System.out.println("worker02手动应答");
};
//接收消息,手动应答 consumerTag这个是表示一旦消息发送被取消将会显示什么
channel.basicConsume(QUEUE_NAME, false, deliverCallback, (consumerTag -> {
System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
}));
}
}
自动应答
package rabbitMQ.work;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import rabbitMQ.util.RabbitMqUtils;
/**
* @author: wu linchun
* @time: 2021/7/8 21:05
* @description: 消费者
*/
public class Worker04 {
private static final String QUEUE_NAME = "ack_001";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
//消息回调
DeliverCallback deliverCallback = (consumerTag, message) -> {
//沉睡5s,模拟其他代码执行
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String receivedMessage = new String(message.getBody(), "UTF-8");
System.out.println("worker04接收到消息:" + receivedMessage);
System.out.println("worker04自动应答");
//手动应答
/**
* 1、消息标记 tag
* 2、是否批量应答 false:不批量应答信道中的消息 true:批量应答信道中的消息
* 由于批量应答可能会导致消息丢失,所以不批量应答,处理一个应答一个
*/
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
};
//接收消息,自动应答 consumerTag这个是表示一旦消息发送被取消将会显示什么
channel.basicConsume(QUEUE_NAME, true, deliverCallback, (consumerTag -> {
System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
}));
}
}
启动消息生产者,并依次启动几个消费者。生产者发送消息,消费者会按照启动顺序依次接收到消息。
注!!!消息手动应答和自动应答的区别
采用手动应答的方式,如果当前接收消息的消费者挂了,那么该消息会被下一个消费者接收,消息是不会丢失的。如果是自动应答,那么如果当前接收消息的消费者挂了,消息就会丢失。
3、rabbitMQ持久化
3.1、队列持久化
没有持久化的队列,一旦rabbitMQ服务宕机或者关闭rabbitMQ,重新启动rabbitMQ后就会被清除的。
![](https://i-blog.csdnimg.cn/blog_migrate/b2e779303f98b1c34d0ec651a952486d.png)
![](https://i-blog.csdnimg.cn/blog_migrate/46935050d513fdaa3b7237c8eccbf783.png)
因此为了防止创建的队列丢失,需要在创建队列时就对其进行持久化。
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
对队列进行持久化以后,在创建队列时就会把队列存入磁盘,这样队列信息就不会丢失了。
3.2、消息持久化
所谓消息持久化是将生产者将消息存入磁盘中,生产者产生的消息默认是存放在内存中的,存放在内存中有个坏处就是如果服务一重启,就会导致消息丢失。为了防止在消息还未发出去时生产者服务宕机导致消息丢失。将消息先持久化到磁盘中,这样即使生产者服务宕机了,还可以从磁盘中获取未发送的消息重新发送。
消息持久化实现:
在生产者的channel.basicPublish设置中添加 MessageProperties.PERSISTENT_TEXT_PLAIN
channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
4、队列分发
4.1、公平分发
公平分发即rabbitMQ默认的轮询分发,把消息一个一个轮流分发给消费者。
实现方式为
channel.basicQos(0); //0表示采用默认轮询分发
4.2、不公平分发
由于不同消费者接受消息处理消息的效率不一样,而rabbitMQ默认是采用轮询的方式依次分发消息给消费者。因此会导致每个消费者被分到的消息是一样的。这会导致处理快的消费者长期处于空闲状态,而处理慢的消费者则会一直不停的在处理并且未处理的消息越累越多。
因此,本着能者多劳的理念,会采用不公平分发,队列优先分配给处理效率高的消费者更多的消息。
根据不同消费者的处理消息能力不同给它们分别设定一些预取值。
例如:生产者一共发8条消息,两个消费者 Consumer01和 Consumer02,分别设置预取值为3和5。那么生产者发送的8条消息中,Consumer01会接收应答3条,Consumer02会接收应答5条。
Consumer01
package rabbitMQ.work;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import rabbitMQ.util.RabbitMqUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author: wu linchun
* @time: 2021/8/9 14:19
* @description: 消费者01
*/
public class Consumer01 {
private static final String QUEUE_NAME = "ack_001";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
//消息回调
//沉睡20s,模拟其他代码
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String receivedMessage = new String(delivery.getBody());
System.out.println("Consumer01接收到消息:" + receivedMessage);
//手动应答
/**
* 1、消息标记 tag
* 2、是否批量应答 false:不批量应答信道中的消息 true:批量应答信道中的消息
* 由于批量应答可能会导致消息丢失,所以不批量应答,处理一个应答一个
* 消息在手动应答是不会丢失的,一旦原本接收消息的消费者挂掉后,该消息会被其他消费者应答
*/
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
//接收消息,自动应答 consumerTag这个是表示一旦消息发送被取消将会显示什么
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
};
//basicQos取值为1表示不公平分发 0表示采用默认轮询分发
channel.basicQos(5);
channel.basicConsume(QUEUE_NAME, false, deliverCallback, cancelCallback);
}
}
Consumer02
package rabbitMQ.work;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import rabbitMQ.util.RabbitMqUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author: wu linchun
* @time: 2021/8/9 14:19
* @description:
*/
public class Consumer02 {
private static final String QUEUE_NAME = "ack_001";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
//消息回调
//沉睡20s,模拟其他代码
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String receivedMessage = new String(delivery.getBody());
System.out.println("Consumer02接收到消息:" + receivedMessage);
//手动应答
/**
* 1、消息标记 tag
* 2、是否批量应答 false:不批量应答信道中的消息 true:批量应答信道中的消息
* 由于批量应答可能会导致消息丢失,所以不批量应答,处理一个应答一个
* 消息在手动应答是不会丢失的,一旦原本接收消息的消费者挂掉后,该消息会被其他消费者应答
*/
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
//接收消息,自动应答 consumerTag这个是表示一旦消息发送被取消将会显示什么
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
};
//basicQos取值为1表示不公平分发 0表示采用默认轮询分发
channel.basicQos(3);
channel.basicConsume(QUEUE_NAME, false, deliverCallback, cancelCallback);
}
}
5、发布确认
5.1、单个确认
单个确认是一种同步确认发布的方式,也就是发布一个消息之后只有它 被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long)这个方法只有在消息被确认 的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。 这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了。
/**
* @author: wu linchun
* @create: 2021/8/9 15:01
* @desc: 单个确认
**/
public static void publishMessageIndividually() throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
//队列声明
String queueName = "ReleaseConfirm";
channel.queueDeclare(queueName, false, false, false, null);
//开启发布确认
channel.confirmSelect();
//开始时间
long begin = System.currentTimeMillis();
//批量发消息
for (int i = 0; i < 1000; i++) {
String message = i + "";
channel.basicPublish("", queueName, null, message.getBytes());
//接收到单个消息就马上进行发布确认
try {
boolean flag = channel.waitForConfirms();
if (flag) {
System.out.println("消息发送成功");
}
} catch (Exception e) {
e.printStackTrace();
}
}
long all = System.currentTimeMillis() - begin;
System.out.println("发布" + 1000 + "个消息,单个确认总共耗时:" + all + "毫秒");
}
5.2、批量消息确认
由于单个消息确认速度非常慢,因此,先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。
/**
* @author: wu linchun
* @create: 2021/8/9 15:46
* @desc: 批量确认
**/
public static void publishMessageBatch() throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
//队列声明
String queueName = "ReleaseConfirm";
channel.queueDeclare(queueName, false, false, false, null);
//开启发布确认
channel.confirmSelect();
//开始时间
long begin = System.currentTimeMillis();
//批量发消息
for (int i = 0; i < 1000; i++) {
String message = i + "";
channel.basicPublish("", queueName, null, message.getBytes());
//每100条消息,进行一下批量确认
if (i % 100 == 0) {
try {
boolean flag = channel.waitForConfirms();
if (flag) {
System.out.println("消息发送成功");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
long all = System.currentTimeMillis() - begin;
System.out.println("发布" + 1000 + "个消息,每100条消息批量确认总共耗时:" + all + "毫秒");
}
5.3、异步消息确认
所谓异步消息确认是指,先只管发消息,消息确认的工作放到后台去做。相当于有两个线程A,B。A线程负责发消息,B负责处理消息。通常会加一个监听事件,把B的处理结果反馈出来。
/**
* @author: wu linchun
* @create: 2021/8/9 16:24
* @desc: 异步确认
**/
public static void publishMessageAsyn() throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
//队列声明
String queueName = "ReleaseConfirm";
channel.queueDeclare(queueName, false, false, false, null);
//开启发布确认
channel.confirmSelect();
//开始时间
long begin = System.currentTimeMillis();
//消息确认成功,回调函数
ConfirmCallback askCallBack = ((deliveryTag, multiple) -> {
System.out.println("确认的消息:" + deliveryTag);
});
//消息确认失败,回调函数
ConfirmCallback naskCallBack = ((deliveryTag, multiple) -> {
System.out.println("未确认的消息:" + deliveryTag);
});
//准备消息的监听器 监听哪些消息成功了,哪些消息失败
//异步通知
channel.addConfirmListener(askCallBack, naskCallBack);
//批量发消息
for (int i = 0; i < 1000; i++) {
String message = i + "";
channel.basicPublish("", queueName, null, message.getBytes());
}
long all = System.currentTimeMillis() - begin;
System.out.println("发布" + 1000 + "个消息,消息异步确认总共耗时:" + all + "毫秒");
}
6、交换机
RabbitMQ 消息传递模型的核心思想是:生产者生产的消息从不会直接发送到队列。生产者将消息发送给交换机,交换机接收到消息后通过Routing Key进行路由,找到队列,将消息送到队列中,队列再把消息送给消费者。
交换机按照类型可分为:直接(direct), 主题(topic) ,标题(headers) , 扇出(fanout)
6.1、fanout(路由模式)
fanout相当于路由广播,就是将接收到的消息发送到所有已知队列中。
消息生产者
package rabbitMQ.exchangeFanout;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;
import rabbitMQ.util.RabbitMqUtils;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;
/**
* @author: wu linchun
* @time: 2021/8/10 11:04
* @description: 发消息 交换机
*/
public class ProducerFanout01 {
//交换机名称
public static final String EXCHANGE_NAME = "ackExchange";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
Scanner input = new Scanner(System.in);
while (!input.hasNext("#")) {
String message = input.next();
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println("生产者发出消息:" + message);
}
}
}
消费者01 / 消费者02
package rabbitMQ.exchangeFanout;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import rabbitMQ.util.RabbitMqUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author: wu linchun
* @time: 2021/8/10 9:40
* @description: 消息接收
*/
public class ReceiveFanout01 {
//交换机名称
public static final String EXCHANGE_NAME = "ackExchange";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
//声明一个交换机 注意这里如果创建的交换机“ackExchange”是持久化的,这里声明该交换机也要持久化
channel.exchangeDeclare(EXCHANGE_NAME, "fanout", true);
//声明一个临时队列,队列名称是随机的,当消费者断开与队列的连接的时候,队列就自动删除
String queueName = channel.queueDeclare().getQueue();
//绑定交换机与队列,因为是fanout所以routingKey无需填写
channel.queueBind(queueName, EXCHANGE_NAME, "");
//接收消息
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("01控制台打印接收到的消息:" + new String(message.getBody(), "UTF-8"));
};
//消费者取消消息时间回调接口
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
6.2、direct(发布/订阅模式)
direct可以指定某条消息要发送给哪个用户,通过绑定某一个队列的Routing Key实现的。一个路由器可以绑定多个队列,消费者可以指定去消费某个队列的消息。
生产者
package rabbitMQ.exchangeDirect;
import com.rabbitmq.client.Channel;
import rabbitMQ.util.RabbitMqUtils;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;
/**
* @author: wu linchun
* @time: 2021/8/10 11:57
* @description:
*/
public class ProducerDirect01 {
public static final String EXCHANGE_NAME = "DirectExchange";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
Scanner input = new Scanner(System.in);
while (!input.hasNext("#")) {
String message = input.next();
channel.basicPublish(EXCHANGE_NAME, "223344", null, message.getBytes("UTF-8"));
System.out.println("生产者发出消息:" + message);
}
}
}
消费者01
package rabbitMQ.exchangeDirect;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import rabbitMQ.util.RabbitMqUtils;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.TimeoutException;
/**
* @author: wu linchun
* @time: 2021/8/10 11:46
* @description:
*/
public class ReceiveDirect01 {
public static final String EXCHANGE_NAME = "DirectExchange";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
//声明一个交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true);
//声明一个队列
channel.queueDeclare("Q1", false, false, false, null);
//绑定队列
channel.queueBind("Q1", EXCHANGE_NAME, "112233");
//接收消息
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("ReceiveDirect01接收到的消息:" + new String(message.getBody(), "UTF-8"));
};
//消费者取消消息时回调接口
channel.basicConsume("Q1", true, deliverCallback, consumerTag -> {
});
}
}
消费者02
package rabbitMQ.exchangeDirect;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import rabbitMQ.util.RabbitMqUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author: wu linchun
* @time: 2021/8/10 11:46
* @description:
*/
public class ReceiveDirect02 {
public static final String EXCHANGE_NAME = "DirectExchange";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
//声明一个交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true);
//声明一个队列
channel.queueDeclare("Q2", false, false, false, null);
//绑定队列
channel.queueBind("Q2", EXCHANGE_NAME, "223344");
//接收消息
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("ReceiveDirect02接收到的消息:" + new String(message.getBody(), "UTF-8"));
};
//消费者取消消息时回调接口
channel.basicConsume("Q2", true, deliverCallback, consumerTag -> {
});
}
}
因为消息只发到了Routing key为:223344的队列Q2,所以只有绑定了223344的消费者02才能拿到消息,消费者01是拿不到的。
6.2.1、多重绑定
所谓多重绑定就是给同一个队列绑定多个Routing Key,作为消费者只需指定需要拿消息的队列名,无需指定该队列的具体的Routing Key,那么就会像广播那样,生产者通过Routing Key往指定队列里面塞消息,消费者只需指定队列名称,就可以拿到该队列的全部消息。当然如果说不同的队列指定了同一个Routing Key,那么消费者只需根据Routing Key就可以拿到指定了同一个Routing Key的不同队列的消息。
生产者
package rabbitMQ.exchangeMulti;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import rabbitMQ.util.RabbitMqUtils;
import java.util.HashMap;
import java.util.Map;
/**
* @author: wu linchun
* @time: 2021/8/10 17:42
* @description: 多重绑定
*/
public class ProducerMulti01 {
private static final String EXCHANGE_NAME = "DirectExchange";
public static void main(String[] argv) throws Exception {
try (Channel channel = RabbitMqUtils.getChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true);
//创建多个 bindingKey
Map<String, String> bindingKeyMap = new HashMap<>();
bindingKeyMap.put("223344", "给Q2,223344");
bindingKeyMap.put("bbccdd", "给Q2,bbccdd");
bindingKeyMap.put("112233", "给Q1,112233");
bindingKeyMap.put("aabbcc", "给Q1,aabbcc");
//qqwwee没有qqwwee这个绑定的RoutingKey去消费这接收这个消息 所以就丢失了
bindingKeyMap.put("qqwwee", "给Q1Q2,qqwwee");
for (Map.Entry<String, String> bindingKeyEntry : bindingKeyMap.entrySet()) {
String bindingKey = bindingKeyEntry.getKey();
String message = bindingKeyEntry.getValue();
channel.basicPublish(EXCHANGE_NAME, bindingKey, null,
message.getBytes("UTF-8"));
System.out.println("生产者发出消息:" + message);
}
}
}
}
消费者01
package rabbitMQ.exchangeMulti;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import rabbitMQ.util.RabbitMqUtils;
/**
* @author: wu linchun
* @time: 2021/8/10 17:44
* @description:
*/
public class ReceiveMulti01 {
private static final String EXCHANGE_NAME = "DirectExchange";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true);
String queueName = "Q1";
channel.queueDeclare(queueName, false, false, false, null);
channel.queueBind(queueName, EXCHANGE_NAME, "112233");
channel.queueBind(queueName, EXCHANGE_NAME, "aabbcc");
System.out.println("等待接收消息.....");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" 接收绑定键 :" + delivery.getEnvelope().getRoutingKey() + ", 消 息:" + message);
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
消费者02
package rabbitMQ.exchangeMulti;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import rabbitMQ.util.RabbitMqUtils;
/**
* @author: wu linchun
* @time: 2021/8/10 17:44
* @description:
*/
public class ReceiveMulti02 {
private static final String EXCHANGE_NAME = "DirectExchange";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true);
String queueName = "Q2";
channel.queueDeclare(queueName, false, false, false, null);
channel.queueBind(queueName, EXCHANGE_NAME, "223344");
channel.queueBind(queueName, EXCHANGE_NAME, "bbccdd");
System.out.println("等待接收消息.....");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" 接收绑定键 :" + delivery.getEnvelope().getRoutingKey() + ", 消 息:" + message);
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
6.3、topic
topic交换机,即“主题交换机”。和direct一样也是支持发布/订阅的,但是topic交换机是支持模糊匹配的。因此topic交换机会被应用在一些搜索功能中,即通过搜索框的关键字去队列中的消息进行模糊匹配,匹配到最终需要的结果。
topic有两个语法规则:*和#
- *(星号)可以代替一个单词
- #(井号)可以替代零个或多个单词
生产者
package rabbitMQ.exchangeTopic;
import com.rabbitmq.client.Channel;
import rabbitMQ.util.RabbitMqUtils;
import java.util.HashMap;
import java.util.Map;
/**
* @author: wu linchun
* @time: 2021/8/10 17:16
* @description:
*/
public class ProducerTopic01 {
private static final String EXCHANGE_NAME = "TopicExchange";
public static void main(String[] argv) throws Exception {
try (Channel channel = RabbitMqUtils.getChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, "topic",true);
/**
* Q1-->绑定的是
* 中间带 orange 带 3 个单词的字符串(*.orange.*)
* Q2-->绑定的是
* 最后一个单词是 rabbit 的 3 个单词(*.*.rabbit)
* 第一个单词是 lazy 的多个单词(lazy.#)
*
*/
Map<String, String> bindingKeyMap = new HashMap<>();
bindingKeyMap.put("quick.orange.rabbit", "被队列 Q1Q2 接收到 *.orange.*");
bindingKeyMap.put("lazy.orange.elephant", "被队列 Q1Q2 接收到 *.orange.*");
bindingKeyMap.put("quick.orange.fox", "被队列 Q1 接收到 *.orange.*");
bindingKeyMap.put("lazy.brown.fox", "被队列 Q2 接收到 lazy.# ");
bindingKeyMap.put("lazy.pink.rabbit", "虽然满足两个绑定但只被队列 Q2 接收一次 lazy.# / *.*.rabbit");
bindingKeyMap.put("quick.brown.fox", "不匹配任何绑定不会被任何队列接收到会被丢弃");
bindingKeyMap.put("quick.orange.male.rabbit", "是四个单词不匹配任何绑定会被丢弃");
bindingKeyMap.put("lazy.orange.male.rabbit", "是四个单词但匹配 Q2 lazy.#");
for (Map.Entry<String, String> bindingKeyEntry : bindingKeyMap.entrySet()) {
String bindingKey = bindingKeyEntry.getKey();
String message = bindingKeyEntry.getValue();
channel.basicPublish(EXCHANGE_NAME, bindingKey, null,
message.getBytes("UTF-8"));
System.out.println("生产者发出消息" + message);
}
}
}
}
消费者01
package rabbitMQ.exchangeTopic;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import rabbitMQ.util.RabbitMqUtils;
/**
* @author: wu linchun
* @time: 2021/8/10 17:18
* @description:
*/
public class ReceiveTopic01 {
private static final String EXCHANGE_NAME = "TopicExchange";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "topic", true);
//声明 Q1 队列与绑定关系
String queueName = "Q1";
channel.queueDeclare(queueName, false, false, false, null);
channel.queueBind(queueName, EXCHANGE_NAME, "*.orange.*");
System.out.println("等待接收消息.....");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" 接收队列 :" + queueName + " 绑定键:" + delivery.getEnvelope().getRoutingKey() + ", 消息:" + message);
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
消费者02
package rabbitMQ.exchangeTopic;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import rabbitMQ.util.RabbitMqUtils;
/**
* @author: wu linchun
* @time: 2021/8/10 17:18
* @description:
*/
public class ReceiveTopic02 {
private static final String EXCHANGE_NAME = "TopicExchange";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "topic", true);
//声明 Q2 队列与绑定关系
String queueName = "Q2";
channel.queueDeclare(queueName, false, false, false, null);
channel.queueBind(queueName, EXCHANGE_NAME, "*.*.rabbit");
channel.queueBind(queueName, EXCHANGE_NAME, "lazy.#");
System.out.println("等待接收消息.....");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" 接收队列 :" + queueName + "绑定键:" + delivery.getEnvelope().getRoutingKey() + ",消息:" + message);
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
7、死信队列
所谓“死信”就是指无法被消费掉的消息。而“死信队列”是指用来专门负责接收那些无法被消费掉的消息的。
死信的来源:
- 消息TTL(time to live)过期
- 队列达到最大长度(队列满了,无法再添加数据到mq中)
- 消息被拒绝(basic.reject 或 basic.nack)并且requeue=false
一个死信队列例子
1、消息过期导致死信
生产者
package rabbitMQ.deadletter;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import rabbitMQ.util.RabbitMqUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author: wu linchun
* @time: 2021/8/22 17:35
* @description:
*/
public class Producer01 {
public static final String NORMAL_EXCHANGE = "normal_exchange";
//普通交换机的名称
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
//死信消息,设置TTL(time to live)时间,过期时间一般是由生产者发消息时指定
AMQP.BasicProperties properties = new AMQP.BasicProperties()
.builder().expiration("10000").build();
for (int i = 0; i < 10; i++) {
String message = "info" + i;
channel.basicPublish(NORMAL_EXCHANGE, "normal", properties, message.getBytes());
}
}
}
消费者
package rabbitMQ.deadletter;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Delivery;
import rabbitMQ.util.RabbitMqUtils;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
/**
* @author: wu linchun
* @time: 2021/8/22 16:28
* @description:
*/
public class Consumer01 {
//死信交换机的名称
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 IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
//
// //声明死信交换机和普通交换机,类型为direct
// channel.exchangeDeclare(NORMAL_QUEUE, BuiltinExchangeType.DIRECT);
// channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
//
//
// //声明普通队列
// Map<String, Object> arguments = new HashMap<>();
// //正常队列设置死信交换机
// arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE);
// //设置死信RoutingKey
// arguments.put("x-dead-letter-routing-key", "dead");
// //声明死信队列和普通队列
// channel.queueDeclare(NORMAL_QUEUE, false, false, false, arguments);
// channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
System.out.println("等待接收消息......");
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println(new String(message.getBody(), "UTF-8"));
};
channel.basicConsume(NORMAL_QUEUE, true, deliverCallback, consumerTag -> {
});
}
}
可以通过代码创建队列/交换机或者自己在rabbitMQ客户端手动创建也行。交换机要绑定队列的routing-key。
只启动生产者,让生产者发送消息,但由于没有消费者来消费消息,因此消息在队列中一旦超时了,就会进入到死信队列中去的。
如果在生产者发的消息还没过期时,有相关的消费者消费了消息,那么这些消息就不会进入到死信队列中去的。
//构建一个消费者,消费队列中的消息
public class Consumer02 {
//普通队列的名称
public static final String NORMAL_QUEUE = "normal_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
DeliverCallback deliverCallback = ((consumerTag, message) -> {
System.out.println("消费者2接收到的消息是:" + new String(message.getBody(), "UTF-8"));
});
channel.basicConsume(NORMAL_QUEUE, true, deliverCallback, consumerTag -> {
});
}
}
2、队列长度达到最大值导致死信
添加一个新的队列,长度为6,并设置死信交换机和死信队列。
交换机绑定该队列
生产者
package rabbitMQ.deadletterOutOfLenQueue;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import rabbitMQ.util.RabbitMqUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author: wu linchun
* @time: 2021/8/22 17:35
* @description:
*/
public class Producer01 {
public static final String NORMAL_EXCHANGE = "normal_exchange";
//普通交换机的名称
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
for (int i = 0; i < 10; i++) {
String message = "info" + i;
channel.basicPublish(NORMAL_EXCHANGE, "normal2", null, message.getBytes());
}
}
}
消费者
package rabbitMQ.deadletterOutOfLenQueue;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import rabbitMQ.util.RabbitMqUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author: wu linchun
* @time: 2021/8/23 22:39
* @description:
*/
public class Consumer02 {
//普通队列的名称
public static final String NORMAL_QUEUE2 = "normal_queue2";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
DeliverCallback deliverCallback = ((consumerTag, message) -> {
System.out.println("消费者2接收到的消息是:" + new String(message.getBody(), "UTF-8"));
});
channel.basicConsume(NORMAL_QUEUE2, false, deliverCallback, consumerTag -> {
});
}
}
因为对normal_queue2设置了队列长度,消费者是从normal_queue2队列中只能拿到6条数据。
生产者一共发了10条消息,超过队列长度的4条都进了死信队列了。
3、消息被拒导致死信队列
新建一个队列,queue3
生产者
package rabbitMQ.deadletterReject;
import com.rabbitmq.client.Channel;
import rabbitMQ.util.RabbitMqUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author: wu linchun
* @time: 2021/8/22 17:35
* @description:
*/
public class Producer01 {
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
for (int i = 0; i < 10; i++) {
String message = "info" + i;
channel.basicPublish(NORMAL_EXCHANGE, "normal3", null, message.getBytes());
}
}
}
消费者
package rabbitMQ.deadletterReject;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import rabbitMQ.util.RabbitMqUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author: wu linchun
* @time: 2021/8/23 22:39
* @description:
*/
public class Consumer02 {
//普通队列的名称
public static final String NORMAL_QUEUE3 = "normal_queue3";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
DeliverCallback deliverCallback = ((consumerTag, message) -> {
String msg = new String(message.getBody(), "UTF-8");
if (msg.equals("info6")) {
System.out.println(new String(message.getBody()) + "是被拒绝的消息");
//requeue=false表示消息被拒后,不放回原队列,而是会放回死信队列
channel.basicReject(message.getEnvelope().getDeliveryTag(), false);
} else {
System.out.println("消费者2接收到的消息是:" + msg);
//应答消息
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
}
});
//消息回调:开启手动应答,因为自动应答的话,消息被拒就会直接丢失掉
channel.basicConsume(NORMAL_QUEUE3, false, deliverCallback, consumerTag -> {
});
}
}
info6这条消息是被拒绝的,因此info6会进入到死信队列中去的。
8、延迟队列
延迟队列,就是用来存放需要在指定时间被处理的消息的队列。通常延迟队列是通过死信队列机制来实现的。延迟队列是死信队列,消息过期的一种。
延迟队列使用场景
- 订单十分钟内未支付则自动取消。
- 用户注册登录后三天内未实名登记则发消息提醒。
- 用户发起退款,如果三天内没有被处理则通知相关运营人员。
8.1、使用TTL死信队列机制实现延迟队列
基于死信队列方式实现延迟队列是通过把消息放到死信队列中,让消费者去消费相关死信队列里的消息。
pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.rabbitmq</groupId>
<artifactId>springboot-rabbitmq</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-rabbitmq</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<repackage.classifier/>
<spring-native.version>0.10.3</spring-native.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--RabbitMQ 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--RabbitMQ 测试依赖-->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
接口发消息
package com.rabbitmq.springbootrabbitmq.controller;
import com.rabbitmq.client.AMQP;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
/**
* @author: wu linchun
* @time: 2021/9/3 21:10
* @description:
*/
@Slf4j
@RequestMapping("ttl")
@RestController
public class SendMsgController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendMsg")
public void sendMsg(@RequestParam("message") String message) {
log.info("当前时间:{},发送一条信息给队列:{}", new Date(), message);
rabbitTemplate.convertAndSend("normal_exchange", "normal", "消息来自 ttl 为 10S 的队列: " + message,
msg -> {
//设置消息过期时间
msg.getMessageProperties().setExpiration("10000");
return msg;
}
);
//rabbitTemplate.convertAndSend("normal_exchange", "normal", "消息来自 ttl 为 40S 的队列: " + message);
}
}
消费者
package com.rabbitmq.springbootrabbitmq.consumer;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Date;
/**
* @author: wu linchun
* @time: 2021/9/3 21:12
* @description:
*/
@Slf4j
@Component
public class DeadLetterQueueConsumer {
//消息监听:监听是否有给当前队列发消息message 消费者是通过监听方式
@RabbitListener(queues = "dead_queue")
public void receiveD(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
}
}
swagger配置
package com.rabbitmq.springbootrabbitmq.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* @author: wu linchun
* @time: 2021/9/2 22:13
* @description:
*/
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket webApiConfig() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("webApi")
.apiInfo(webApiInfo())
.select()
.build();
}
private ApiInfo webApiInfo() {
return new ApiInfoBuilder()
.title("rabbitmq 接口文档")
.description("本文档描述了 rabbitmq 接口定义")
.version("1.0")
.contact(new Contact("Atlantide", "xxx",
"13917052985@163.com"))
.build();
}
}
application.yml
server:
port: 8082
spring:
profiles:
active: dev
application-dev.yml
server:
port: 8082
spring:
#mq配置
rabbitmq:
host: 127.0.0.1
port: 5672
virtual-host: /
username: admin
password: 123456
测试一下
但是这样子的延迟队列是存在一个问题的,就是如果先后发了一条ttl为30s的消息以及一条ttl为10s的消息。由于队列是“先进先出”的特性,因此那条ttl为10s的消息发出后,即使到了10s,因为前面有一条ttl为30s的消息没发出去会挡住ttl为10s的消息的。这就造成了堵塞。ttl为10s的消息只有等ttl为30s的消息发出去后,才会发出去。
因此为了解决这个问题,需要安装一个rabbitMQ插件:rabbitmq_delayed_message_exchange-3.9.0.ez
下载地址:Releases · rabbitmq/rabbitmq-delayed-message-exchange · GitHub
把下载好的.ez文件放到rabbitMQ安装目录下的plugins文件夹下。
进入plugins目录,使用命令:rabbitmq-plugins enable rabbitmq_delayed_message_exchange
安装该插件。
安装完成后,重启rabbitMQ,打开客户端,在新建交换机的Type选项栏会新出现“x-delayed-message”。
8.2、使用rabbitMQ插件实现延迟队列
不同于死信队列方式实现延迟队列是通过把消息放到死信队列中。使用rabbitMQ插件实现延迟队列是通过创建一个“延迟交换机”,由生产者设定消息延迟时间,把消息给“延迟交换机”,让“延迟交换机”来实现延迟发送消息。
实现方式:
首先创建一个“延迟交换机”
绑定一个队列
消息生产者接口代码(设定延迟时间用setDelay(1000)方法)。
@ApiOperation(value = "发一条延迟10s的消息(TTL)")
@GetMapping("/sendMsg2")
public void sendMsg2(@RequestParam("message") String message) {
log.info("当前时间:{},发送一条信息给队列:{}", new Date(), message);
rabbitTemplate.convertAndSend("delayed.exchange", "normal.delayed", "消息来自 ttl 为 10S 的队列: " + message,
msg -> {
//设置消息过期时间
msg.getMessageProperties().setDelay(10000);
return msg;
}
);
}
@ApiOperation(value = "发一条延迟30s的消息(TTL)")
@GetMapping("/sendMsg3")
public void sendMsg3(@RequestParam("message") String message) {
log.info("当前时间:{},发送一条信息给队列:{}", new Date(), message);
rabbitTemplate.convertAndSend("delayed.exchange", "normal.delayed", "消息来自 ttl 为 30S 的队列: " + message,
msg -> {
//设置消息过期时间
msg.getMessageProperties().setDelay(30000);
return msg;
} //noargs
);
}
消费者
package com.rabbitmq.springbootrabbitmq.consumer;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Date;
/**
* @author: wu linchun
* @time: 2021/9/4 17:29
* @description:
*/
@Slf4j
@Component
public class TTLQueueConsumer {
//消息监听:监听是否有给当前队列发消息message 消费者是通过监听方式
@RabbitListener(queues = "normal_queue")
public void receiveT(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("当前时间:{},收到TTL队列信息{}", new Date().toString(), msg);
}
}
测试一下,先后发送ttl为30s和ttl为10s的两条消息。
可以发现ttl为30s的消息并没有堵塞ttl为10s的消息。
8.3、@RabbitHandler注解
@RabbitHandler注解通常会和@RabbitListener注解连用。@RabbitListener可以标注在类上,用来替该类监听某个队列,而@RabbitHandler则标注在@RabbitListener标注类中的方法上。可以根据监听到的队列的传入消息的不同类型,选择用不同的方法进行处理。
控制层接口:
@ApiOperation(value = "发一条String类型的普通消息")
@GetMapping("/sendMsg4")
public void sendMsg4(@RequestParam("message") String message) {
log.info("当前时间:{},发送一条String信息给队列:{}", new Date(), message);
rabbitTemplate.convertAndSend("normal_exchange", "normal", message);
}
@ApiOperation(value = "发一条Map类型的普通消息")
@PostMapping("/sendMsg5")
public void sendMsg5(@RequestBody Map<Object, Object> map) {
log.info("当前时间:{},发送一条Map信息给队列:{}", new Date(), map);
rabbitTemplate.convertAndSend("normal_exchange", "normal", map);
}
消息接收者:
package com.rabbitmq.springbootrabbitmq.consumer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.Map;
/**
* @author: wu linchun
* @time: 2021/9/13 19:16
* @description:
*/
@Slf4j
@Component
@RabbitListener(queues = "normal_queue")
public class Receiver01 {
@RabbitHandler
public void processMessage1(String msg) {
log.info("当前时间:{},收到String类型的队列信息{}", new Date().toString(), msg);
}
@RabbitHandler
public void processMessage2(Map<Object, String> map) {
log.info("当前时间:{},收到Map类型的队列信息{}", new Date().toString(), map);
}
}
9、总结
以上是我通过学习尚硅谷的rabbitMQ的课程整理的一些笔记。消息队列中消息监听器其实就是有些类似于设计模式中的“观察者模式”(当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新)。给类/方法添加@RabbitListener指定监听哪个队列。当队列中有消息时,监听者会把该消息给到所有监听了该队列的类或者方法上。目前在工作中对RabbitMQ的应用主要就是对于@RabbitListener和@RabbitHandler这两个注解的使用。在一个添加了@RabbitListener的进行队列消息监听的方法中。首先会接收并处理接收到的队列消息,手动应答该消息,最后判断该消息是否是重复发送的?如果是重发送的就直接拒绝该消息,否则如果不是重复的消息,就把消息放回队列重新消费。
之所以会有重发消息的情况是由于可能出现的网络波动等原因导致消息传递失败,因此会重发消息。对于重发的消息可能不止一条,因此要把重复投递的消息过滤掉。
10、参考资料
Controller层中接受Get方式传来的参数 - 。妖妖灵 - 博客园https://www.cnblogs.com/xiongweilong/p/14144215.html
RabbitMQ中各种消息类型如何处理 - ws珍惜现在 - 博客园 (cnblogs.com)https://www.cnblogs.com/fdzfd/p/9319481.html
消息队列RabbitMQ应答模式(自动、手动)_art_code的博客-CSDN博客https://blog.csdn.net/art_code/article/details/90509626