如何保证RabbitMQ不被重复消费,幂等性?
先说为什么会重复消费:正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除;
但是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。
其实遇到这种问题根本不用慌,先看业务逻辑是做啥的,如果仅仅只是对数据库的新增相关操作,直接判断下就可以了,如果已经存在就修改。不存在就插入新数据。
如果需要对数据做唯一判断的话性,可以在写入消息队列的数据做唯一标示,消费消息时,根据唯一标识判断是否消费过;
如何保证RabbitMQ消息的可靠传输?
答:消息不可靠的情况可能是消息丢失,劫持等原因;
丢失又分为:生产者丢失消息、消息列表丢失消息、消费者丢失消息;
生产者丢失消息:
消息队列丢数据:消息持久化。
处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。
这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。
这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。
那么如何持久化呢?
这里顺便说一下吧,其实也很容易,就下面两步
- 将queue的持久化标识durable设置为true,则代表是一个持久的队列
- 发送消息的时候将deliveryMode=2
这样设置以后,即使rabbitMQ挂了,重启后也能恢复数据
消费者丢失消息:消费者丢数据一般是因为采用了自动确认消息模式,改为手动确认消息即可!
消费者在收到消息之后,处理消息之前,会自动回复RabbitMQ已收到消息;
如果这时处理消息失败,就会丢失该消息;
解决方案:处理消息成功后,手动回复确认消息。
一、RabbitMQ的安装
众所周知,RabbitMQ
的安装相对复杂,需要先安装Erlang,再按着对应版本的RabbitMQ的服务端,最后为了方便管理还需要安装rabbitmq_management管理端插件,偶尔还会出现一些安装配置问题,故十分复杂。 在开发测试环境下使用docker
来安装就方便多了,省去了环境和配置的麻烦。
1. 拉取官方image
docker pull rabbitmq:management
2. 启动RabbitMQ
docker run -dit --name MyRabbitmq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin -p 15672:15672 -p 5672:5672 rabbitmq:management
rabbitmq:management: image:tag --name:指定容器名;
-d:后台运行容器;
-t:在新容器内指定一个伪终端或终端;
-i:允许你对容器内的标准输入 (STDIN) 进行交互;
-p:指定服务运行的端口(5672:应用访问端口;15672:控制台Web端口号);
-e:指定环境变量;(RABBITMQ_DEFAULT_USER:默认的用户名;RABBITMQ_DEFAULT_PASS:默认用户名的密码);
至此RabbitMQ就安装启动完成了,可以通过http://localhost:15672 登陆管理后台,用户名密码就是上面配置的admin/admin
二、使用SpringBoot自动创建队列
1. 引入amqp包
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
2. MQ配置
bootstrap.yml 配置
spring:
rabbitmq:
host: localhost
port: 5672
virtual-host: /
username: admin
password: admin
listener:
simple:
concurrency: 5
direct:
prefetch: 10
concurrency
:每个listener在初始化的时候设置的并发消费者的个数 prefetch
:每次从一次性从broker里面取的待消费的消息的个数
rabbitmq-spring.xml配置
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd"> <!--接收消息的队列名--> <rabbit:queue name="login-user-logined" /> <!--声明exchange的名称与类型--> <rabbit:topic-exchange name="login_barryhome_fun"> <rabbit:bindings> <!--queue与exchange的绑定和匹配路由--> <rabbit:binding queue="login-user-logined" pattern="login.user.logined"/> </rabbit:bindings> </rabbit:topic-exchange> </beans>
rabbit:topic-exchange
:声明为topic消息类型 pattern="login.user.logined"
:此处是一个表达式,可使用“*”表示一个词,“#”表示一个或多个词
3. 消息生产端
@Autowired RabbitTemplate rabbitTemplate; @GetMapping("/send") public LoginUser SendLoginSucceedMessage(){ LoginUser loginUser = getLoginUser("succeed"); // 发送消息 rabbitTemplate.convertAndSend(MessageConstant.MESSAGE_EXCHANGE, MessageConstant.LOGIN_ROUTING_KEY, loginUser); return loginUser; } @NoArgsConstructor @AllArgsConstructor public class LoginUser implements Serializable { String userName; String realName; String userToken; Date loginTime; String status; }
这里需要注意的是默认情况下消息的转换器为SimpleMessageConverter
只能解析string和byte,故传递的消息对象必须是可序列化的,实现Serializable
接口
SimpleMessageConverter only supports String, byte[] and Serializable payloads, received: fun.barryhome.cloud.dto.LoginUser
4. 消息消费端
@Component public class ReceiverMessage { @RabbitListener(queues = "login-user-logined") public void receiveLoginMessage(LoginUser loginUser) { System.err.println(loginUser); } }
@RabbitListener(queues = "login-user-logined")
:用于监听名为login-user-logined 队列中的消息
5. 自动创建Queue
@SpringBootApplication @ImportResource(value = "classpath:rabbitmq-spring.xml") public class MQApplication { public static void main(String[] args) { SpringApplication.run(MQApplication.class, args); } }
在没有导入xml且MQ服务器上没有列队的情况下,会导致找不到相关queue的错误
channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no queue 'login-user-logined' in vhost '/', class-id=50, method-id=10)
而导入之后将自动创建
exchange和queue
消息重试
默认情况下如果有消息消费出错后会一直重试,造成消息堵塞
如图可观察unacked和total一直是1,但deliver/get飙升
消息堵塞之后也影响到后续消息的消费,时间越长越来越多的消息将无法及时消费处理。
如果是单条或极少量的消息有问题可通过多开节点concurrency
将正常的消息消息掉,但如果较多则全部节点都将堵塞。
如果想遇到消息消费报错重试几次就舍弃,从而不影响后续消息的消费,如何实现呢?
spring:
rabbitmq:
host: localhost
port: 5672
virtual-host: /
username: admin
password: admin
listener:
simple:
concurrency: 5
prefetch: 10
retry:
enabled: true # 允许消息消费失败的重试
max-attempts: 3 # 消息最多消费次数3次
initial-interval: 2000 # 消息多次消费的间隔2秒
以上配置允许消息消费失败后重试3次,每次间隔2秒,如果还是失败则直接舍弃掉本条消息。 重试可解决因非消息体本身处理问题产生的临时性的故障,而将处理失败的消息直接舍弃掉只是为其它消息正常处理的权益之计而以,将业务操作降到相对低的影响。
五、死信队列
死信队列就是当业务队列处理失败后,将消息根据routingKey转投到另一队列,这样的情况有:
- 消息被拒绝 (basic.reject or basic.nack) 且带 requeue=false不重新入队参数或达到的retry重新入队的上限次数
- 消息的TTL(Time To Live)-存活时间已经过期
- 队列长度限制被超越(队列满,queue的"x-max-length"参数)
1. 修改rabbitmq-spring.xml
<!--接收消息的队列名--> <rabbit:queue name="login-user-logined"> <rabbit:queue-arguments> <entry key="x-message-ttl" value="10000" value-type="java.lang.Long"/> <!--死信的交换机--> <entry key="x-dead-letter-exchange" value="login_barryhome_fun"/> <!--死信发送的路由--> <entry key="x-dead-letter-routing-key" value="login.user.login.dlq"/> </rabbit:queue-arguments> </rabbit:queue> <rabbit:queue name="login-user-logined-dlq"/> <!--申明exchange的名称与类型--> <rabbit:topic-exchange name="login_barryhome_fun"> <rabbit:bindings> <!--queue与exchange的绑定和匹配路由--> <rabbit:binding queue="login-user-logined" pattern="login.user.logined"/> <rabbit:binding queue="login-user-logined-dlq" pattern="login.user.login.dlq"/> </rabbit:bindings> </rabbit:topic-exchange>
通过对死信发送的交换机和路由的的设置,可将消息转向具体的queue中。这里交换机可以和原业务队列不是一个。 当login-user-logined
中的消息处理失败后将直接转投向login-user-logined-dlq
队列中。 当程序逻辑修复后可再将消息再移回业务队列中move messages
2. 安装插件
如图提示需要先安装插件
3. 移动消息
安装成功后就可以输入业务队列名再转投