RabbitMQ安装与基本原理
【1】安装下载
(1.1)从官网进入到下载界面,找到想要的版本
参考自:https://blog.csdn.net/qq_45790384/article/details/122765960
如上图,找对于的平台,我们是 centos7,就点击如上图这个;
我要找老版本 3.8.9,所以点击 github
如上图,对应下翻慢慢找就找到了我们要的 3.8.9 版本
(1.2)快速下载安装(rpm)
RabbitMq 下载 :https://github.com/rabbitmq/rabbitmq-server/tags
Erlang 对应关系:https://www.rabbitmq.com/which-erlang.html
Erlang 包下载:https://github.com/rabbitmq/erlang-rpm/tags
操作如下:
#yum -y install make gcc gcc-c++ kernel-devel m4 ncurses-devel openssl-devel glibc-devel xmlto perl wget socat
yum -y install socat rpm -ivh erlang-23.3.3-1.el7.x86_64.rpm
# 输入 erl 核验是否安装好 rpm -ivh rabbitmq-server-3.8.9-1.el7.noarch.rpm systemctl enable rabbitmq-server systemctl start rabbitmq-server systemctl status rabbitmq-server
# 后台方式启动参考
./rabbitmq-server -detachedservice
rabbitmq-server start
查阅进程情况:
停止服务
- systemctl stop rabbitmq-server / systemctl stop rabbitmq-server stop
(1.3)用户管理
RabbitMQ安装成功后有一个默认用户和密码,都是guest,但这个guest用户只能在RabbitMQ本机登录使用,如果想在其他机器上访问RabbitMQ需要添加新的账户。
在RabbitMQ安装目录的sbin目录下,使用下面命令添加一个admin用户,并且密码也是admin:
# 添加用户 rabbitmqctl add_user admin admin #然后修改用户角色为管理员 rabbitmqctl set_user_tags admin administrator #最后给admin用户添加权限 rabbitmqctl set_permissions -p / admin ".*" ".*" ".*"
# 查看当前用户列表
rabbitmqctl list_users
-
重置命令
- 关闭应用的命令为
- rabbitmqctl stop_app
- 清楚命令为
- rabbitmqctl reset
- 重新启动的命令为
- rabbitmqctl start_app
- 关闭应用的命令为
(1.4)开启 web 管理插件
# 注意,要是 rabbitmq 服务存活才行
rabbitmq-plugins enable rabbitmq_management
报错如下:
{:query, :rabbit@ctl, {:badrpc, :timeout}}
hostnamectl # 查看主机名 [root@db1 ~]# hostnamectl Static hostname: db1 Transient hostname: ctl 如上,主机名我们静态设置的是 db1,但它实际当前是ctl hostnamectl set-hostname db1 # 修改主机名
rabbitmq-plugins enable rabbitmq_management
curl 127.0.0.1:15672
# 如果还有问题,则需改 /etc/hosts 如下,然后再次尝试启动
127.0.0.1 host
192.168.190.128 db1
结果如下图:(注意如果访问不了,那就要看防火墙之类的情况了 如 firewalld / iptables / SELINUX 等等)
我们输入之前创建的 admin 账户,最终登录进去 得到下图
(1.5)【查阅】常用命令
# 前台启动Erlang VM和RabbitMQ rabbitmq-server
# 后台启动 rabbitmq-server -detached
# 停止RabbitMQ和Erlang VM rabbitmqctl stop
# 查看所有队列 rabbitmqctl list_queues
# 查看所有虚拟主机 rabbitmqctl list_vhosts
# 在Erlang VM运行的情况下启动RabbitMQ应用 rabbitmqctl start_app rabbitmqctl stop_app
# 查看节点状态 rabbitmqctl status
# 查看所有可用的插件 rabbitmq-plugins list
# 启用插件 rabbitmq-plugins enable <plugin-name>
# 停用插件 rabbitmq-plugins disable <plugin-name>
# 添加用户 rabbitmqctl add_user username password
# 列出所有用户: rabbitmqctl list_users
# 删除用户: rabbitmqctl delete_user username
# 清除用户权限: rabbitmqctl clear_permissions -p vhostpath username
# 列出用户权限: rabbitmqctl list_user_permissions username
# 修改密码: rabbitmqctl change_password username newpassword
# 设置用户权限: rabbitmqctl set_permissions -p vhostpath username ".*" ".*" ".*"
# 创建虚拟主机: rabbitmqctl add_vhost vhostpath
# 列出所以虚拟主机: rabbitmqctl list_vhosts
# 列出虚拟主机上的所有权限: rabbitmqctl list_permissions -p vhostpath
# 删除虚拟主机: rabbitmqctl delete_vhost vhost vhostpath
# 移除所有数据,要在 rabbitmqctl stop_app 之后使用: rabbitmqctl reset
【2】RabbitMQ的工作模式及原理
(2.0)5大核心概念概述
这些选项是什么意思呢?它们是用来干什么的呢?
这就是本篇文章要讲的RabbitMQ的5大核心概念:
Connection(连接)、Channel(信道)、Exchange(交换机)、Queue(队列)、Virtual host(虚拟主机)。
在介绍这些概念之前,我们先看一张图,图中展示的是RabbitMQ的工作模型,根据这张图,下面理解起来就比较容易了
其中,中间的Broker表示RabbitMQ服务,每个Broker里面至少有一个Virtual host虚拟主机,每个虚拟主机中有自己的Exchange交换机、Queue队列以及Exchange交换机与Queue队列之间的绑定关系Binding。
producer(生产者)和consumer(消费者)通过与Broker建立Connection来保持连接,然后在Connection的基础上建立若干Channel信道,用来发送与接收消息。
Connection(连接)
每个producer(生产者)或者consumer(消费者)要通过RabbitMQ发送与消费消息,首先就要与RabbitMQ建立连接,这个连接就是Connection。Connection是一个TCP长连接。
Channel(信道)
Channel是在Connection的基础上建立的虚拟连接,RabbitMQ中大部分的操作都是使用Channel完成的,比如:声明Queue、声明Exchange、发布消息、消费消息等。
看到此处,你是否有这样一个疑问:既然已经有了Connection,我们完全可以使用Connection完成Channel的工作,为什么还要引入Channel这样一个虚拟连接的概念呢?因为现在的程序都是支持多线程的,如果没有Channel,那么每个线程在访问RabbitMQ时都要建立一个Connection这样的TCP连接,对于操作系统来说,建立和销毁TCP连接是非常大的开销,在系统访问流量高峰时,会严重影响系统性能。
Channel就是为了解决这种问题,通常情况下,每个线程创建单独的Channel进行通讯,每个Channel都有自己的channel id帮助Broker和客户端识别Channel,所以Channel之间是完全隔离的。
Connection与Channel之间的关系可以比作光纤电缆,如果把Connection比作一条光纤电缆,那么Channel就相当于是电缆中的一束光纤。
Virtual host(虚拟主机)
Virtual host是一个虚拟主机的概念,一个Broker中可以有多个Virtual host,每个Virtual host都有一套自己的Exchange和Queue,同一个Virtual host中的Exchange和Queue不能重名,不同的Virtual host中的Exchange和Queue名字可以一样。
这样,不同的用户在访问同一个RabbitMQ Broker时,可以创建自己单独的Virtual host,然后在自己的Virtual host中创建Exchange和Queue,很好地做到了不同用户之间相互隔离的效果。
Queue(队列)
Queue是一个用来存放消息的队列,生产者发送的消息会被放到Queue中,消费者消费消息时也是从Queue中取走消息。
Exchange(交换机)
Exchange是一个比较重要的概念,它是消息到达RabbitMQ的第一站,主要负责根据不同的分发规则将消息分发到不同的Queue,供订阅了相关Queue的消费者消费到指定的消息。那Exchange有哪些分发消息的规则呢?这就要说到Exchange的4种类型了:direct、fanout、topic、headers。
在介绍这4种类型的Exchange之前,我们先来了解一下另外一个比较重要的概念:Routing key,翻译成中文就是路由键。
当我们创建好Exchange和Queue之后,需要使用Routing key(通常叫作Binding key)将它们绑定起来,producer在向Exchange发送一条消息的时候,必须指定一个Routing key,然后Exchange接收到这条消息之后,会解析Routing key,然后根据Exchange和Queue的绑定规则,将消息分发到符合规则的Queue中
【3】Exchange 的 4种类型
接下来,我们根据上面的流程再来详细介绍下4种类型的Exchange。
(3.1)direct(直接匹配)
direct的意思是直接的,direct类型的Exchange会将消息转发到指定Routing key的Queue上,Routing key的解析规则为精确匹配。也就是只有当producer发送的消息的Routing key与某个Binding key相等时,消息才会被分发到对应的Queue上。
比如我们现在有一个direct类型的Exchange,它下面绑定了三个Queue,Binding key分别是ORDER/GOODS/STOCK:
然后我们向该Exchange中发送一条消息,消息的Routing key是ORDER:
按照规则分析,这条消息应该被路由到MY_EXCHANGE_ORDER_QUEUE这个Queue。
消息发送成功之后,我们去Queues中查看,发现确实只有MY_EXCHANGE_ORDER_QUEUE这个QUEUE接收到了一条消息。
进入这个队列,通过getMessage取出消息查看,确实是我们刚才手动发送的那条消息。
所以,direct类型的Exchange在分发消息时,必须保证producer发送消息的Routing key与Exchange和Queue绑定的Binding key相等才可以。
(3.2)fanout(广播)
fanout是扇形的意思,该类型通常叫作广播类型。fanout类型的Exchange不处理Routing key,而是会将发送给它的消息路由到所有与它绑定的Queue上。
比如我们现在有一个fanout类型的Exchange,它下面绑定了三个Queue,Binding key分别是ORDER/GOODS/STOCK:
然后我们向该Exchange中发送一条消息,消息的Routing key随便填一个值abc:
按照规则分析,这条消息应该被路由到所有与该Exchange绑定的Queue,即三个Queue都应该会受到消息。
消息发送成功之后,我们去Queues中查看,发现确实每个QUEUE都接收到了一条消息。
进入这三个QUEUE,通过getMessage取出消息查看,确实是我们刚才手动发送的那条消息。
所以,fanout类型的Exchange不管Routing key是什么,它都会将接收到的消息分发给所有与自己绑定了的Queue上。
(3.3)topic(主题,通配符)
topic的意思是主题,topic类型的Exchange会根据通配符对Routing key进行匹配,只要Routing key满足某个通配符的条件,就会被路由到对应的Queue上。通配符的匹配规则如下:
● Routing key必须是一串字符串,每个单词用“.”分隔;
● 符号“#”表示匹配一个或多个单词;
● 符号“*”表示匹配一个单词。
例如:“*.123” 能够匹配到 “abc.123”,但匹配不到 “abc.def.123”;“#.123” 既能够匹配到 “abc.123”,也能匹配到 “abc.def.123”。
比如我们现在有一个topic类型的Exchange,它下面绑定了4个Queue,Binding key分别是 *.ORDER、GOODS.*、#.STOCK、USER.#。
然后我们向该Exchange中发送一条消息,消息的Routing key为:USER.ABC.ORDER。
按照规则分析,USER.ABC.ORDER这个Routing key只可以匹配到 “USER.#” ,所以,这条消息应该被路由到MY_TOPIC_USER_QUEUE这个Queue中。消息发送成功之后,我们去Queues中查看,发现结果符合我们的预期。
进入这个QUEUE,通过getMessage取出消息查看,确实是我们刚才手动发送的那条消息。
(3.4)headers(参数条件匹配)
日常工作中,以上三种类型的Exchange已经能够满足我们基本上所有的需求了,headers模式并不经常使用,我们只需要对headers Exchange有一个基本的了解就可以了。
headers Exchange中,Exchange与Queue之间的绑定不再通过Binding key绑定,而是通过Arguments绑定。
比如我们现在有一个headers类型的Exchange,下面通过不同的Arguments绑定了三个Queue:
producer在发送消息时可以添加headers属性,Exchange接收到消息后,会解析headers属性,只要我们上面配置的Arguments中的所有属性全部被包含在Headers中并且值相等,那么这条消息就会被路由到对应的Queue中。
比如我们向上面的Exchange中发送一条消息,消息的Headers中添加“x=1”:
根据规则,只有queue1这个队列满足x=1的条件,queue2中的y=2条件不满足,所以,消息应该只被路由到queue1队列中。
消息发送成功后,我们可以看到queue1确实收到了消息:
并且这条消息就是我们刚才手动发送的消息:
然后我们再发送一条消息,消息的headers中有两个属性:x=1,y=2:
根据规则,queue1的x=1的条件满足,queue2的x=1、y=2的条件满足,queue3的y=2的条件满足,所以,这三个Queue应该都能够收到这条消息。消息发送成功后,结果符合预期:
这条消息就是我们刚才手动发送的消息:
看到这里,我们已经对RabbitMQ的核心概念有了清晰的理解,并且对它们的工作模式也都探索清楚了
RabbitMQ系列的相关内容及项目中的实战使用,比如如何使用RabbitMQ实现订单超时未支付自动取消订单?如何使用RabbitMQ实现分布式事务?等等。
【4】RabbitMq 的消息一致性
(4.1)MQ 消息的工作模式
- 生产者将消息发送给RabbitMQ的Exchange交换机;
- Exchange交换机根据Routing key将消息路由到指定的Queue队列;
- 消息在Queue中暂存,等待消费者消费消息;
- 消费者从Queue中取出消息消费。
通过这种工作模式,很好地做到了两个系统之间的解耦,并且整个过程是一个异步的过程,producer发送消息后就可以继续处理自己业务逻辑,不需要同步等待consumer的消费结果。
但任何一项技术的引入,除了带来它自身的优点之外,必然也会带来其他的一些缺点。MQ消息中间件虽然可以做到系统之间的解耦以及异步通信,但可能会存在消息丢失的风险。
(4.2)MQ 常规工作模式存在的问题——消息可能丢失的情况
什么是消息丢失呢?简单来说,就是producer发送了一条消息出去,但由于某种原因(比如RabbitMQ宕机了),导致consumer没有消费到这条消息,最终导致producer与consumer两个系统的数据与期望结果不一致。
那消息是如何丢失的呢?既然在RabbitMQ的工作模式中,一条消息从producer到达consumer要经过4个步骤,那么在这4步中,任何一步都可能会把消息丢掉:
(1)生产者将消息发送给Exchange交换机:
假如producer向Exchange发送了一条消息,由于是异步调用,所以producer不关心Exchange是否收到了这条消息,就继续向下处理自己的业务逻辑。如果在Exchange收到消息之前,RabbitMQ宕机了,那这条消息就丢了。
(2)Exchange交换机接收到消息后,会根据producer发送过来的Routing key将消息路由到匹配的Queue队列中。
一般情况下,这一步不会出现什么问题,因为这一步是在RabbitMQ内部实现的,并且Exchange与Queue之间的Routing key都会在开发之前约定好;
所以,只要保证producer发送消息时使用的Routing key是真实存在的即可正确地路由到指定的Queue队列。
但万一小明在复制代码的时候,手一抖,导致发送消息时的Routing key多了个数字,此时,消息发出去后,Exchange虽然能收到消息,但由于匹配不到Routing key,所以无法将消息路由到Queue队列,那这条消息也算是变相消失了。
(3)消息到达Queue中暂存,等待consumer消费:
如果消息成功被路由到了Queue中,此时这条消息会被暂存在RabbitMQ的内存中,等到consumer消费,假如在consumer消费这条消息之前,RabbitMQ宕机了,那么这条消息也会丢失。
(4)consumer从Queue中取走消息消费:
如果前面一切顺利,并且消息也成功被consumer从Queue中取走消费,但consumer最后消费发生异常失败了。
由于默认情况下,当一条消息被consumer取走后,RabbitMQ就会将这条消息从Queue中直接删除,所以,即使consumer消费失败了,这条消息也会消失,这样也会导致producer与consumer两个系统的数据不一致。
(4.3)解决方法—概述
分析完了消息可能发生丢失的几个步骤,接下来就可以针对这几个步骤进行逐个解决,来保证消息不会丢失,也就是消息的可靠性投递与消费。
1. 保证producer发送消息到RabbitMQ broker的可靠性
通过上面的分析,我们知道,producer发送消息到broker的过程中,丢失消息的原因是producer发送完消息之后,就主观认为消息发送成功了,即使RabbitMQ发生故障导致没有接收到消息,producer也是无法知道的。所以,要保证producer发出去的消息100%被broker接收成功,我们需要让producer发送消息后知道一个结果,然后根据这个结果再做相应的处理。
RabbitMQ提供了两种方式来达到这一目的:一种是Transaction(事务)模式,一种是Confirm(确认)模式。
(4.4)Transaction模式保证数据一致性
Transaction模式类似于我们操作数据库的操作,首先开启一个事务,然后执行sql,最后根据sql执行情况进行commit或者rollback。
在RabbitMQ中实现Transaction模式时,首先要用Channel对象的txSelect()方法将信道设置成事务模式,broker收到该命令后,会向producer返回一个select-ok的命令,表示信道的事务模式设置成功;然后producer就可以向broker发送消息了。在消息发送完成后,producer要调用Channel对象的commit()方法提交事务。整个流程可以用下图表示:
在Transaction模式中,producer只有收到了broker返回的Commit-Ok命令后才能提交成功,若在commit执行之前,RabbitMQ发生故障抛出异常,producer可以将其捕获,然后通过Channel对象的txRollback()方法回滚事务,同时可以重发该消息。
Transaction模式虽然可以保证消息从producer到broker的可靠性投递,但它的缺点也很明显,它是阻塞的,只有当一条消息被成功发送到RabbitMQ之后,才能继续发送下一条消息,这种模式会大幅度降低RabbitMQ的性能,不推荐使用。
(4.5)Confirm模式保证数据一致性(推荐)
针对Transaction模式存在的浪费RabbitMQ性能的问题,RabbitMQ推出了Confirm模式。Confirm模式是一个异步确认的模式,producer发送一条消息后,在等待确认的过程中可以继续发送下一条消息。
要使用Confirm模式,首先要通过Channel对象的confirmSelec()方法将当前Channel设置为Confirm模式,然后,通过该Channel发布消息时,每条消息都会被分配一个唯一的ID(从1开始计数),当这条消息被路由到匹配的Queue队列之后,RabbitMQ就会发送一个确认(ack)给producer(如果是持久化的消息,那么这个确认(ack)会在RabbitMQ将这条消息写入磁盘之后发出),这个确认消息中包含了消息的唯一ID,这样producer就可以知道消息已经成功到达Queue队列了。
当RabbitMQ发生故障导致消息丢失,也会发送一个不确认(nack)的消息给producer,nack消息中也会包含producer发布的消息唯一ID,producer接收到nack的消息之后,可以针对发布失败的消息做相应处理,比如重新发布等。
了解了原理后,接下来看下代码层面实现 Confirm 模式的三种方式:
(1)单条确认方式
单条确认模式中,每发送一条消息后,通过调用Channel对象的waitForConfirms()方法等待RabbitMQ端确认,主要代码如下:
这种方式实际上是一种同步等待的方式,只有当一条消息被确认之后,才能发送下一条消息,所以,实际使用中不推荐这种方式。
(2)批量确认方式
批量确认方式与单条确认方式使用方法类似,只是将确认的步骤放到了最后,可以一次性发送多条消息,最后统一确认,主要代码如下:
waitForConfirmsOrDie()方法会等最后一条消息被确认或者得到nack时才会结束,这种方式虽然可以做到多条消息并行发送,不用互相等待;
但最后确认的时候还是通过同步等待的方式完成的,所以也会造成程序的阻塞,并且当有任意一条消息未确认就会抛出异常,实际使用中不推荐这种方式。
(3)异步确认方式(推荐)
异步确认方式的实现原理是在将Channel设置为Confirm模式后,给该Channel添加一个监听ConfirmListener,ConfirmListener中定义了两个方法,一个是handleAck,用来处理RabbitMQ的ack确认消息,一个是handleNack,用来处理RabbitMQ的nack未确认消息,这两个方法会在RabbitMQ完成消息确认和发生故障导致消息丢失时回调,producer接收到回调时可以执行对应的回调处理。主要代码如下:
异步确认的方式效率很高,多条消息既可以同时发送,不需要互相等待,又不用同步等待确认结果,只需异步监听确认结果即可,所以,实际使用中推荐使用这种方式。
(4.6) 保证Exchange 路由消息到 Queue 的可靠性
上面分析了,当producer发送消息时,由于小明手抖,导致消息的Routing key是一个不存在的key,从而变相丢失的情况,要如何提前规避掉呢?有两种方式:ReturnListener和使用备胎Exchange交换机。
(1)ReturnListener
ReturnListener是一个监听器,作用于Channel信道上,当producer发送一条消息给RabbitMQ后,如果由于Routing key不存在导致消息不可成功到达Queue队列,RabbitMQ就会将这条消息发送回producer的ReturnListener,在ReturnListener的handleReturn方法中,producer可以针对退回的消息做处理。
要使用ReturnListener,在发送消息时要注意,在basicPublish的方法中有一个mandatory的入参,只有将该参数值设置为true才可以正常使用ReturnListener,否则,当Routing key不存在时,消息会被自动丢弃。核心代码如下:
producer运行上述代码之后,就会打印出ReturnListener中的信息,此时,producer可以针对这条消息做业务处理,比如发送提醒信息给相关人员处理,或者更新状态等。但要注意,这里最好不要重发ReturnListener中的消息,因为导致消息被回退的原因就是消息不可达,如果在ReturnListener中重发这条消息的话,那么就有可能进入一个死循环,重发->退回->重发->退回......
(2)备胎Exchange交换机
除了使用ReturnListener,我们还可以使用备胎交换机的方式来解决Routing key不存在导致消息不可达的问题。所谓备胎交换机,是指当producer发送消息的Routing key不存在导致消息不可达时,自动将这条消息转发到另一个提前指定好的交换机上,这台交换机就是备胎交换机。备胎交换机也有自己绑定的Queue队列,当备胎交换机接到消息后,会将消息路由到自己匹配的Queue队列中,然后由订阅了这些Queue队列的消费者消费。
在开发时,如果要使用备胎交换机,也要在发送消息时,将mandatory参数值设置为true,否则,消息就会由于不可达而被RabbitMQ自动丢弃。核心代码如下:
然后我们运行该程序,然后可以在RabbitMQ控制台看到,消息被成功路由到了备胎交换机绑定的Queue队列:
然后我们开启一个消费者消费该Queue,也可以正常消费到这条原本不可达的消息:
(4.7) 保证Queue消息存储的可靠性
消息到达Queue队列之后,在消费者消费之前,RabbitMQ宕机也会导致消息的丢失,所以,为了解决这种问题,我们需要将消息设置成持久化的消息。
持久化消息会写入RabbitMQ的磁盘中,RabbitMQ宕机重启后,会恢复磁盘中的持久化消息。
但消息是存储于Queue队列中的,所以,只把消息持久化也不行,也要把Queue也设置成持久化的队列,这样,RabbitMQ宕机重启之后,Queue才不会丢失;
否则,即使消息是持久化的,但Queue不是持久化的,那么RabbitMQ重启之后,Queue都不存在了,那么消息也就无处存放,也就相当于没了。
通过代码设置消息持久化和队列持久化很简单,首先看队列持久化:
Channel对象在声明队列的时候,第二个参数 durable 就代表该队列是否持久化队列,设置为 true 表示当前队列是持久化队列。
然后看下消息持久化:
Channel对象在发送消息时,有一个BasicProperties类型的参数,该参数中可以设置一些消息的属性,其中就包括是否持久化的 deliveryMode 属性,2表示持久化消息。
将队列和消息进行持久化可以保证大部分场景下RabbitMQ宕机重启后消息不丢失,但并不能100%保证,因为RabbitMQ接收到持久化消息之后,并不会立刻将消息存入磁盘,而是有一个缓冲buffer,只有当buffer写满了或者每25ms一次才会将数据写入磁盘中,所以,在这之前,消息还是会存在丢失的可能,想要更大程度地保证这种情况下消息不丢失,可以搭建RabbitMQ镜像集群,这个我在以后章节会讲。
上面介绍了队列和消息的持久化,其实Exchange交换机也可以持久化,不过交换机是否持久化对消息的可靠性并没有什么影响,只是非持久化的交换机在RabbitMQ重启之后也会消失,那么producer向该交换机发送消息时就可能会有问题,所以,一般情况下,建议也将交换机持久化:
Channel对象在声明交换机时,有一个durable的参数,该参数设置为true即表示该交换机为持久化交换机。
(4.8) 保证consumer消费消息的可靠性
consumer消费消息时,有一个ack机制,即向RabbitMQ发送一条ack指令,表示消息已经被成功消费,RabbitMQ收到ack指令后,会将消息从本地删除。默认情况下,consumer消费消息是自动ack机制,即消息只要到达consumer,就会向RabbitMQ发送ack,不管consumer是否消费成功。所以,为了保证producer与consumer数据的一致性,我们要使用手动ack的方式确认消息消费成功,即在消息消费完成后,通过代码显式调用发送ack。
首先,我们一起看下实现手动ack的核心代码:
consumer向RabbitMQ发送ack时有三种形式:
(1)reject:表示拒收消息。发送拒收消息时,需要设置一个 requeue 的参数,表示拒收之后,这条消息是否重新回到RabbitMQ的Queue之后,设置为true表示是,false表示否(消息会被删除)。若 requeue 设置为 true,那么消息回归原Queue之后,会被消费者重新消费,这样就会出现死循环,消费->拒绝->回Queue->消费->拒绝->回Queue......所以,一般设置为false。如果设置为true,那么最好限定消费次数,比如同一条消息消费5次之后就直接丢掉。
(2)nack:一般consumer消费消息出现异常时,需要发送nack给MQ,MQ接收到nack指令后,会根据发送nack时设置的requeue参数值来判断是否删除消息,如果requeue为true,那么消息会重新放入Queue队列中,如果requeue为false,消息就会被直接删掉。当requeue设置为true时,为了防止死循环性质的消费,最好限定消费次数,比如同一条消息消费5次之后就直接丢掉。
(3)ack:当consumer成功把消息消费掉后,需要发送ack给MQ,MQ接收到ack指令后,就会把消费成功的消息直接删掉。
(4.9)补偿机制解决 producer 与 consumer 的绝对一致
经过上面这几个步骤的改造优化,我们的应用程序已经能够保证99.99%场景下消息的可靠性投递与消费了,但由于某些不可控因素,也并不能保证100%的消息可靠性,只有producer明确知道了consumer消费成功了,才能100%保证两边数据的一致性。
因为MQ是异步处理,所以producer是无法通过RabbitMQ知道consumer是否消费成功了,所以,如果要保证两边数据100%一致,consumer在消费完成之后,要给producer发送一条消息通知producer自己消费成功了。
但producer不能一直在那干等着,如果consumer过了1小时还没有发送消息给producer,那么很可能是consumer消费失败了,所以,producer与consumer之间要根据业务场景定义一个反馈超时时间,并在producer后台定义一个定时任务,定时扫描超过指定时间未接收到consumer确认的消息,然后重发消息。重发消息的逻辑中,最好定义一个重发最大次数,比如重发3次后还是不行的话,那可能就是consumer有bug或者发生故障了,就停止重发,等待问题查明再解决。
既然producer可能会重发消息,所以,consumer端一定要做幂等控制(就是已经消费成功的消息不再次消费),要做到幂等控制,消息体中就需要有一个唯一的标识,consumer可以根据这个唯一标识来判断自己是否已经成功消费了这条消息。
【参考文档】
【4】部分参考:https://baijiahao.baidu.com/s?id=1734155728227361181