| 源码(码云):https://gitee.com/yin_zhipeng/rabbit-mq-demo.git |
|---|
安装rabbitmq,可以参考,https://blog.youkuaiyun.com/grd_java/article/details/119696892,其它的内容没必要参考这篇文章
本文根据官方文档的6大模式,依次将知识点带出,比如工作模式中,多个消费者消费消息的消息丢失问题,可以通过手动应答解决。
一、相关概念和介绍
应用场景
- 流量削峰:如果系统的处理能力无法满足用户的请求数量。可能会导致系统瘫痪。虽然可以当系统处理能力达到上限时,限制用户请求,让用户无法操作。此时消息队列可以做一个缓冲,让这些请求分散开排好队,依次的进行处理。但是消息队列会
- 应用解耦:消息队列可以异步的执行任务,比如要买菜,买肉,不用异步的话,需要先买肉,然后等肉切好,再去买菜。解耦后,先买肉,让肉先切着,我同时去买菜。如果菜买回来,肉没切完,那就等肉。如果肉腐烂了,坏了。那可以重新挑肉,或者直接退钱,回家告诉孩子,今天做不了饭,肉腐烂了。
- 异步处理:可以想象为,你在等水烧开的时候,顺便扫地。
常见MQ
- ActiveMQ
- 优点:单机吞吐量万级,时效性ms级,可用性高,基于主从架构实现高可用,消息可靠性较高(不容易丢失数据)
- 缺点:比较老了,官方社区(Apache)对ActiveMQ 5.x维护越来越少,高吞吐量场景使用较少。
- Kafka:为大数据而生,百万级TPS的吞吐量,让它在数据采集,传输,存储过程中发挥出色,被LinkedIn,Uber,Twitter,Netflix等大公司采纳。
- 优点:性能卓越,单机写入TPS约百万条/秒,吞吐量高。时效性ms级,可用性非常高(分布式,一个数据多个副本,少数机器宕机,不会数据丢失,或不可用),消息有序,通过控制可以保证所有消息被消费且仅被消费一次,日志领域比较成熟,功能较为简单,支持简单的MQ功能,大数据领域的实时计算和日志采集被大规模使用。
- 缺点:队列越多,load越高,消息发送响应时间越长,短轮询方式,实时性取决于轮询时间间隔,消费失败不支持重试,一台代理宕机,消息会乱序,社区更新慢。
- RocketMQ:阿里巴巴开源产品,Java实现,参考Kafka并改进,广泛应用与订单,交易,充值,流计算,消息推送,日志流式处理,binglog分发等场景。
- 优点:单机吞吐量十万级,可用性高,分布式架构,消息可以做到0丢失,功能较为完善,扩展性好,支持10亿级别消息堆积。
- 缺点:支持的客户端语言少,目前只有java和c++,c++不成熟,社区活跃度一般,没有在MQ核心中实现JMS等接口,系统要迁移,有些需要修改大量代码。
- RabbitMQ:2007年发布,基于AMQP(高级消息队列协议),可复用企业消息系统,当前最主流的消息中间件之一。
- 优点:erlang语言编写(因此高并发特性好,性能好,吞吐量万级),MQ功能比较完备,健壮,稳定,易用,跨平台,支持多种语言,支持AJAX文档齐全,管理界面好用,社区活跃度高,更新频率高。
- 缺点:贵,商业版需要收费,学习成本高。
1. 四大核心
- 生产者:就是生产消息的,也就是发快递的,消息可以想象为快递。
- RabbitMQ:也就是快递站,处理快递,具体邮寄到哪里
- 交换机:邮寄的策略管理,主要管理一张路由表,比如这个快递要发到北京东城区,它会匹配路由表,然后根据管理策略,找合适的快递员(队列)。比如:“我这里有个包裹,你们谁来邮寄配送一下?”,再比如“1号队列,你现在还能处理包裹吗?满了是吧!2号队列,你能吗?可以是吧?给你!!!”
- 队列:就是运输快递的快递员的车,它会将包裹交到消费者手里。交换机管理多个快递员,当然这些快递员需要是自己家的快递员(具有绑定关系的队列)。比如顺丰快递不能管人家韵达快递的快递员。
- 消费者:也就是等快递的你,快递来了,你自己处理,直接用了,或者回馈一些,评个论,退个货啥的。
2. 名词介绍
- Borker:就是RabbitMQ实体,接收和分发消息的应用。RabbitMQ Server就是Message Broker
- Virtual host:虚拟主机,多个不同用户使用同一个RabbitMQ server提供的服务时,可以划分多个vhost,每个用户在自己的vhost创建exchange/queue等。是出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中,类似网络中的namespace概念。
- Connection:publisher/consumer和broker之间的TCP连接
- Channel:建立TCP连接开销很大,效率也低,Channel是TCP Connection内部建立的逻辑链接,多线程下,每个线程单独创建channel通讯(AMQP method中包含channel id帮助客户端和message broker识别channel),channel之间完全隔离,非常的轻量级。不用每次访问RabbitMQ都建立TCP connection。
- Exchange:message到达broker第一站,根据分发规则,匹配查询表中的routing key,分发消息到queue中,常用类型direct(点到点)、topic(发布-订阅)、fanout(广播)
- Queue:快递员的车,消息都在这里,等consumer消费者取走。
- Binding:exchange和queue之间的虚拟链接,也就是路由表,可以包含路由key(routing key)和Binding绑定信息,保存到exchange交换机的路由表中,用于message的分发依据
二、6大模式的前5个
1. 简单模式
整体架构为一个生产者P(Producer),消息队列(不用交换机,就用一个队列),和一个消费者C(consumer),效果就是P发给队列一个消息,C取一个消息
- 代码位置
- 模块依赖
<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>
- 生产者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("消息发送完毕!!!");
}
}
- 消费者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 效果实现
- 代码位置
- 生产者代码:生产多条消息到队列
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+"\"发送完毕!!!");
}
}
}
- 消费者代码,多个消费者消费消息
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确认包。
- 肯定确认:Channel.basicAck()。RabbitMQ将知道该消息处理成功,可以将其丢弃。
- 否定应答:Channel.basicNack()。代表处理失败,不可以丢弃。
- 拒绝,驳回:Channel.basicReject()。代表不处理该消息了,可以将其丢弃。
basicNack()和basicReject()的区别是前者多一个参数Multiple,可以批量应答。basicAck()也可以批量应答。减少网络堵塞。批量应答的效果是针对channel信道的,整个channel信道的所有消息,都会批量的一次性进行应答。
- true:表示批量应答channel上未应答的消息,比如channel上有传送tag的消息5,6,7,8。当前处理的tag是8。那么此时如果basicAck()进行批量应答,5-8这些没有应答的消息,都会被确认收到消息应答。
- false:也就是不批量应答,当前处理tag是8,就只会应答8这个tag。5-7这三个消息依然不会被应答。
所以,建议,使用手动应答,并且不要使用批量应答,对每一个消息,单独进行处理。
消息自动重新入队
如果消息没有发送ACK确认,RabbitMQ将认为消息没有完全处理,会将其重新排队,其它消费者如果可以处理,将很快分发给另一个消费者。
2.3 消息应答机制改造
代码位置:
实现效果:线程0可以处理所有消息,需要1秒时间,线程1需要11秒时间,遇到消息test会处理失败,进行拒绝应答。线程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+"\"发送完毕!!!");
}
}
}
- 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();

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














最低0.47元/天 解锁文章
1万+

被折叠的 条评论
为什么被折叠?



