RabbitMQ应用问题
上篇文章:
RabbitMQ—事务与消息分发https://blog.csdn.net/sniper_fandc/article/details/149312189?fromshare=blogdetail&sharetype=blogdetail&sharerId=149312189&sharerefer=PC&sharesource=sniper_fandc&sharefrom=from_link
目录
1 幂等性保证
1.1 概念和常见的幂等性
1.1.1 数据库
1.1.2 应用程序
1.1.3 网络通信
1.2 MQ幂等性保证
1.2.1 为什么会出现同一条消息被多次消费
1.2.2 如何保证幂等性
2 顺序性保证
2.1 顺序性问题
2.2 顺序性保证
3 消息积压
3.1 原因
3.2 解决方案
1 幂等性保证
1.1 概念和常见的幂等性
幂等性是指同一种操作多次应用,结果始终和第一次应用的结果一样。
应用到程序中,就是对同一个程序按照相同参数进行调用,无论调用多少次,结果总是一样的。
1.1.1 数据库
select操作是幂等的,因为如果在没有其它程序修改的情况下,多次执行select结果都一样。但是如果在这一过程中执行了修改操作,select查询的结果就不一样,这并不意味着select操作不是幂等的,因为幂等性指对资源的影响,而不是对结果的影响。
delete操作是幂等的,因为多次对同一个数据进行删除,多次执行的结果都是该数据消失,而不会是其它结果比如该数据没被删除。
update通常也是幂等的,比如对某个值的更新总是update users set name='John' where id=1,那么多次执行该记录的name列结果总为John。但是也有非幂等的情况,比如update users set age=age+1 where id=1,那么多次执行每次结果均不一样(i++等自增自减操作同理)。
insert是非幂等的,多次插入同一条记录,会插入重复数据。
1.1.2 应用程序
比如订单系统,应该保证接口的幂等性。同一个订单多次调用同一个接口进行支付,应该保证成功扣一次款。
1.1.3 网络通信
HTTP协议的GET请求就满足幂等性,多次请求一个页面,得到的结果一样,因此GET请求的页面可以被浏览器缓存、收藏等等。
但是由于GET请求功能强大,现在使用该请求通常会完成其它请求的任务(POST),因此不同场景下GET请求就不一定满足幂等性。
1.2 MQ幂等性保证
在MQ中,幂等性是指同一条消息被多次消费,对系统影响是一样的。
1.2.1 为什么会出现同一条消息被多次消费
一般MQ中消息传输保证有三个等级:
1.At most once:最多一次,消息最多发送一次,无论成功还是失败(消息可能丢失)。
2.At least once:最少一次,消息最少发送一次,如果失败会重发(消息可能重复传输)。
3.Exactly once:恰好一次,消息必定被成功传递且仅被处理一次。
目前RabbitMQ能保证最多一次和最少一次,恰好一次无法保证。对于最少一次,如果消息处理成功但是ACK因为网络等问题没有返回(生产者-MQ的发布确认模式、MQ-消费者的消息可靠性保证机制(手动确认)),消息就会重发,因此就会出现消费者对同一条消息多次处理的情况(同一订单多次扣款)。
1.2.2 如何保证幂等性
1.全局唯一ID
为消息设置全局唯一的ID值,比如使用UUID或MQ消息中的唯一ID。当消费者接收到消息后先判断该ID值的消息是否已经被消费过,如果还未消费过,就进行处理,处理结束后并将ID值保存起来。如果已经消费过,就放弃该消息。
注意:保存ID值可以使用Redis的setnx命令,把ID值作为key,该命令原子性且能判断ID是否已经存在(不存在才设置),即实现了幂等性。
2.业务逻辑判断
在业务中首先查询是否已经存在相关数据或已经修改相关数据,比如insert前先select,这两个操作通过事务打包成原子性操作,避免重复插入。也可以使用乐观锁(带version号),修改时判断版本号低于当前数据的版本号,说明要修改的数据已经被其它线程修改过,就不再修改。
2 顺序性保证
2.1 顺序性问题
顺序性问题是指消费者消费消息的顺序和生产者生产消息的顺序是一致的。有些场景要求顺序性,比如连续对同一个个人信息进行多次修改,结果应该是最后一次修改的结果,但是如果没有顺序性,结果就无法预料了。
一下是常见的一些影响顺序性的原因:
(1)同一个队列的消费者有多个:队列向消费者分发消息是并行进行的,因此一些顺序性的消息就被分发到不同消费者,不同消费者处理速度不一样,就无法保证顺序性。
(2)网络问题:如果消息处理的ACK因为网络问题丢失,连接断开,就可能导致消息重新入队,重新入队后消息的消费顺序性就无法保证。
(3)消息重试:如果消息处理发生异常或ACK丢失(auto级别的SpringBoot的消息可靠机制),就会出发RabbitMQ的消息重试,也会导致顺序性无法保证。
(4)消息路由问题:如果交换机和队列的绑定关系复杂,那么消费就可能因为复杂的路由被分发到不同队列,从而导致顺序性无法保证。
(5)死信队列:消息过期后被放入死信队列,死信队列的消费由其它消费者订阅,消息处理时间也就无法确定,因此顺序性无法保证。
2.2 顺序性保证
顺序性保证分为全局顺序性保证和局部顺序性保证:
全局顺序性保证是指消息的消费顺序在全局内是一致的(多个队列多个消费者之间),比如上图中要求消息按照1、2、3、4、5、6的顺序进行消息。那Consumer2只能等Consumer1消费完消息1再消费消息2。
局部顺序性保证只要求队列内的消息消费顺序是一致的(单个队列内),比如在Queue1,消息只需要按照1、3、6的顺序消费,和2、4、5消息的顺序无关。
根据顺序性的全局和局部,就有不同的保证策略:
(1)全局顺序性保证
1.全局只有一个队列和一个消费者,由于队列先进先出的特性,消息的顺序性自然得到保证。但是缺点就是吞吐量太低。
2.为每个消费都设置全局序列号,在消费端根据序列号顺序进行消费。缺点是业务实现复杂,吞吐量也低。
(2)局部顺序性保证
1.分区消费:通过多个不同的分区,仅保证分区内的顺序性,不同分区由不同消费者消费。比如可以使用hash算法对消息进行计算,根据计算结果把消息分配给不同的队列。实际场景中,就是对同一用户的修改信息发送到同一个队列,对同一订单的消息也发送给同一个队列。
注意:RabbitMQ并没有实现分区消费,需要在业务层使用代码保证(spring-cloud-stream等)。
2.消息确认机制:本质上也是保证同一个队列的消息可靠性和顺序性,因为只有上一条消息的ACK收到后(手动确认),队列才会推送下一条消息。
上述策略不是只使用一条就可以完全保证顺序性,实际使用往往是多条策略的结合。
注意:既然选择保证顺序性,就意味系统性能(吞吐量、响应时间等)一定会下降。RabbitMQ主要场景保证吞吐量和可用性,而不保证顺序性,因此需要考虑业务场景来进行选择。
3 消息积压
3.1 原因
消息积压指待处理的消息数量超过消费者处理能力,从而导致消息在队列中越来越多。通常有如下原因:
1.生产者生产消息过快:高流量或高负载场景下,比如秒杀场景,生产者发送消息的速率远超过消费者处理能力。
2.消费者处理速度过慢:可能消费者所在机器的配置较低,CPU、内存和磁盘等硬件资源限制了速率;也可能代码逻辑复杂,处理流程多导致处理速度较慢;还可能消费端代码本身效率就慢;最后还可能因为消息处理发生异常,导致消息无法得到确认,不断重试或重新入队,从而导致其它消息无法及时处理。
3.网络问题:网络问题导致消费者无法及时接收到消息从而导致消息积压。
4.RabbitMQ服务器配置低:比如交换机队列配置的数量太少,服务本身的路由速度或其它功能速度就慢。
3.2 解决方案
解决消息积压问题的首要原则是根据测试来寻找消息积压的原因,然后根据原因对症下药:
1.针对生产者生产消息过快的原因,可以限制生产者发送速率,比如对生产者限流;或者根据消费者能力在代码中动态调整发送速率;还可以对消息设置过期时间,防止消息存在时间过长导致积压,过期的消息进入死信队列,让其它消费者专门处理。
2.针对消费者处理速度过慢的原因,可以增加消费者数量,比如增加机器;也可以优化代码逻辑,使用多线程等技术并发处理;还可以使用消息分发的限流机制,用prefetchCount设置符合消费者处理速度上限的值,然后让待处理的消息分发到其它能够处理的消费者;最后就是设置合适的重试策略(重试一定次数就不再重试),并把一直失败的消息转入死信队列。
3.针对RabbitMQ配置低的原因,提高RabbitMQ服务所在机器的硬件配置条件,并设置合适的配置参数,调整队列、交换机的数量和结构。
下篇文章: