文章目录
1. 消息如何保证100%投递成功
1.1 什么是生产端的可靠性投递
- 保障消息的成功发出
- 保障MQ节点的成功接收
- 发送端收到MQ节点(Broker)确认应答
- 完善的消息进行补偿机制
1.2 实现可靠性投递的方案
1.2.1 消息落库,对消息状态进行打标
![](https://i-blog.csdnimg.cn/blog_migrate/8ea7ef2dd0e4956a4952b3ad100cc6e9.png)
发送消息的流程:
- 生产者将业务数据和消息数据保存到数据库
- 给消费者发送消息
- 消费者受到消息后,返回确认消息
- 生产者更改消息的状态
- 定时任务定时检查状态
- 如果状态是消费者接受失败,重新发送消息
- 如果重试次数大于3次,不再重试,直接讲消息抛弃
存在的问题:因为发送一次消息,会操作两次数据库,如果在高并发的场景下,会频繁的操作数据库,对数据库造成很大压力
1.2.2 消息的延迟投递,做二次确认,回调检查
![](https://i-blog.csdnimg.cn/blog_migrate/2eaae2e6d7608a9d823367576afe3570.png)
发送消息的流程:
生产者将业务数据保存到数据库
2. 如何避免消息的重复消费问题
首先说名一下为什么会出现重复消费的问题,是因为消息又重回队列了,当任务超时或没有及时返回状态或出现异常等都会引起任务重回队列,然后重新消费。
首先先讲一下什么是幂等性:一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外)。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同,具体参见:https://www.cnblogs.com/javalyy/p/8882144.html,针对此问题,对不同的业务场景可以采用不同的方案:
- 拿到消息后对数据库进行insert操作,利用数据库主键去重,给这个消息做一个唯一主键,那么就算出现重复消费,也会导致主键冲突,避免数据库出现脏数据
- 拿到这个消息做redis的set的操作,那就容易了,不用解决,因为你无论set几次结果都是一样的,set操作本来就算幂等操作
- 以redis为例,给消息分配一个全局id,只要消费过该消息,将<id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。
3. Confirm确认消息
3.2 实现机制
- 消息的确认:生产者投递消息后,如果brocker收到消息,则会给生产者一个应答
- 生产者进行接受应答,用来确定这条消息是否正常的发送到brocker(如果写入了rabbitmq中,rabbitmq会给你回传一个ack消息,告诉你说这个消息ok了。如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,你可以重试)
3.2 confirm确认消息流程解析
![](https://i-blog.csdnimg.cn/blog_migrate/d90a6ca4c89de05a6141ceb49962f8ea.png)
3.3 Confirm确认消息的实现
-
在channel开启确认模式:channel.confirmSelect()
-
在channel上添加监听:channel.addConfirmListener(),监听成功和失败的返回结果,根据具体的结果对消息进行重新发送或记录日志等后续处理
channel.addConfirmListener(new ConfirmListener() { //成功(deliverTay消息唯一标签) @Override public void handleAck(long deliveryTag, boolean multiple) throws IOException { System.err.println("------ack! ------"); } //失败 @Override public void handleNack(long l, boolean b) throws IOException { System.err.println("------no ack!-------"); }
});
这样confirm机制就解决了数据丢失的问题
4. Return返回消息
4.1 实现机制
Return Listener用于处理一些不可路由的消息在某些情况下,如果我们在发送消息时,当前RouteKey找不到,这时需要监听这些不可达的消息,就使用Return Listener(必须先添加监听,再发送消息)
4.2 Return消息流程解析
![](https://i-blog.csdnimg.cn/blog_migrate/fce3c409c5a41787b329aecf8c461a25.png)
4.3 实现
-
在channel添加监听,channel.addReturnListener()
channel.addReturnListener(new ReturnListener() { @Override public void handleReturn(int replayCode, String replayText,String exchange, String routingKey,AMQP.BasicProperties basicProperties, byte[] body) throws IOException { // 消息不可达时进行的处理的逻辑 } });
-
Mondatory:如果为true,则监听会接收到路由不可达的消息,然后进行处理,如果设为false,brocker自动删除该消息
channel.basicPublish(交换机名,routingKey,true,属性,发送的消息);
5. 自定义Consumer
使用consumer.nextDelivery()可以读取到queue中的下一条消息,但是queue中有多条消息时,就要使用while循环了,这样不方便,但我们可以自定义Consumer会更方便,解耦行也更强,下面时自定义Consumer实现:
public class MyConsumer extends DefaultConsumer{
public MyConsumer(Channel channel) {
super(channel);
}
@Override
public void handleDelivery(String consumerTag,Envelope envelope,AMQP.BasicProperties basicProperties,byte[] body) throws IOException {
}
}
6. 消息的消费端限流
6.1 什么是消费端限流
RabbitMQ提供了一种QOS(服务质量保证)功能,即在非自动确认消息的前提下,如果一定数据的消息(通过基于consumer或者channel设置QOS值)未被确认前,不进行消费新的消息
6.2 为什么要限流
- 假设一个场景,我们Rabbitmq服务器有上万条未处理的消息,我们随便打开一个消费者客户端,会出现这种情况:巨量的消息瞬间全部推送过来,但是我们单个客户端无法同时处理这么多数据!此时很有可能导致服务器崩溃,严重的可能导致线上的故障。
- 除了这种场景,还有一些其他的场景,比如说单个生产者一分钟生产出了几百条数据,但是单个消费者一分钟可能只能处理60条数据,这个时候生产端和消费端肯定是不平衡的。通常生产端是没办法做限制的。所以消费端肯定需要做一些限流措施,否则如果超出最大负载,可能导致消费端性能下降,服务器卡顿甚至崩溃等一系列严重后果。
6.3 实现方式
修改prefetch_count和no_ask=false值,即channel.basicQos(0,1,false);只有关闭自动应答的情况下,限流才生效,自动应答情况下是无效的
7. 消息的ACK与重回队列
7.1 消费端ACK
消费段进行消费时,如果由于业务异常我们可以进行日志记录,然后进行补偿;或如果服务器宕机等严重问题,我们就需要手工进行ACK保障消费段消费成功
7.2 消费端重回队列
重回队列是为了对没有处理成功的消息,把消息重新传递给Brocker,在实际应用中,都会关闭重回队列,即设为false
8. TTL消息
Time To Live缩写,即生存时间。RabbitMQ提供了2中过期时间机制:
- 消息的过期时间,在消息发送时可以指定过期时间
- 队列的过期时间,在消息入队开始计算,只要超过了队列的超时时间配置,消息就会自动消息
9. 死信队列DLX
9.1 什么是DLX
Dead-Letter-Exchange,利用DLX,当消息在一个队列中变成死信(dead message)之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX.
9.2 消息变为死信的情况
- 消息被拒绝(basic.reject/basic.nack)且requeue=false
- 消息TTL过期
- 队列达到最大长度
9.3 实现机制
DLX是一个正常的Exchange,能在任何队列上被指定,当队列中有死心时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上,进而被路由到另一个队列,这样就可以监听这个队列中的消息做相应的处理
9.4 实现方式
-
首先设置死信队列的exchange和queue,并进行绑定(Exchange:dlx.exchange;Queue:dlx.change;RoutingKey:#)
-
然后正常声明交换机,队列,绑定,但是要在队列加上 agruments.put(“x-dead-letter-exchange”,“dlx.exchange”);
Map<String,Object> agruments = new HashMap<>(); agruments.put("x-dead-letter-exchange","dlx.exchange"); //**这个agruments属性,要设置到声明队列上 channel.queueDeclare(queueName,true,false,false,agruments);
这样消息在过期、队列达到最大长度时,消息就可以自动路由到死信队列
9.5 区分重回队列和死信队列
重回队列是处理消费段没有消费成功的消息,让这些消息重回队列;而死信队列是出现上面3种情况,才会重新发送给另一个Exchange,进而路由到另一个队列
消息生产到消费整个过程的可靠性总结
- 生产者到Brocker出现网络问题:如果发送不成功,写个for循环重试N次,N次之后还不行,写入数据库持久化,发消息通知人,手动处理
- 消息能到brocker,但是找不到对应的交换机:使用Confirm监听机制,写个for循环重试N次,N次之后还不行,写入数据库持久化,发消息通知人,手动处理
- 消息从exchange无法路由到对应的queue,比如routingkey写错了:使用Retrun监听机制,写个for循环重试N次,N次之后还不行,写入数据库持久化,发消息通知人,手动处理
- 消费手动确认,RabbitMQ有重试机制,重试N次之后,自动进入死信队列,可以监听死信队列,死信队列中一旦有消息,发消息通知人,手动处理
如何保证消息队列的顺序性
先说一下为什么要保证消息的顺序性,先举个例子:假设,签到送积分,签到和送积分是分开的,签到的逻辑是向签到表中插入一条记录,送积分的逻辑是判断积分所对应的签到记录是否存在,存在则送积分,否则不送,假设消息队列中有两条消息,一条是签到消息,另一条是送积分消息,这条积分消息带着签到记录的id,那么,送积分的时候就要依赖是否有签到记录。并且现在我们系统,大多是集群模式,存在多个消费者或多个生产者,我们采用的Redis分布式锁来保证消息队列的顺序和消费的顺序;(这个只是个人理解)