消息队列(MQ)本身的作用、如下图:
秒杀系统中,消息队列(MQ)的主要作用是削峰、解耦、异步处理、增强系统的并发处理能力
**解耦:**系统组件之间通过消息队列进行解耦,我们只需要依赖于mq,避免了各个子系统间的强依赖问题,提高系统的可扩展性和可维护性。
**异步处理:**消息队列可以实现异步处理,用户提交订单后,可以直接返回结果,无需等待秒杀后续流程完成。这样能避免总耗时比较长,从而提高用户体验。
**削峰:**突然出现的请求峰值,导致系统不稳定的问题。使用mq后,由于消费者的消费能力有限,会按照自己的节奏来消费消息,多的请求不处理,保留在mq的队列中,不会对系统的稳定性造成影响,能够起到消峰的作用。
MQ消息队列存在的主要问题包括消息丢失、消息顺序问题、消息堆积、重复消费、延时及过期失效问题。
消息丢失:
消息在生产、传输、消费过程中可能会因为各种原因丢失
1、生产者发送失败(网络原因)
2、MQ服务器处理失败(mq服务器持久化时,磁盘出现异常)
3、消费者处理失败(消费者确认失败,消费者确认超时,业务处理失败)
问题解决:
1、可以采取的措施包括增加消息发送表记录,通过定期检查待确认消息的状态来重新发送丢失的消息。
1)增加一张消息发送表,当生产者发完消息之后,会往该表中写入一条数据,状态status标记为待确认。如果消费者读取消息之后,调用生产者的api更新该消息的status为已确认。
2)有个job,每隔一段时间检查一次消息发送表,如果5分钟(这个时间可以根据实际情况来定)后还有状态是待确认的消息,则认为该消息已经丢失了,重新发条消息。但是如果由于某些原因,消息消费者下单一直失败,一直不能回调状态变更接口,这样job会不停的重试发消息。最后,会产生大量的垃圾消息。需要每次在job重试时,需要先判断一下消息发送表中该消息的发送次数是否达到最大限制,如果达到了,则直接返回。如果没有达到,则将次数加1,然后发送消息
3)这样不管是由于生产者、mq服务器、还是消费者导致的消息丢失问题,job都会重新发消息。
2、开启生产者确认机制,确保生产者的消息能到达队列,如果报错可以先记录到日志中,再去修复数据。开启持久化功能,确保消息未消费前在队列中不会丢失,其中的交换机、队列、消息都要做持久化。(这种方式可以辅助)
消息顺序问题:
1、kafka同一个partition中能保证顺序,但是不同的partition无法保证顺序。
2、rabbitmq的同一个queue能够保证顺序,但是如果多个消费者同一个queue也会有顺序问题。
3、消费者使用多线程消费消息,也无法保证顺序。
4、消费消息时同一个订单的多条消息中,中间的一条消息出现异常情况,顺序将会被打乱。
5、生产者发送到mq中的路由规则,跟消费者不一样,也无法保证顺序。
问题解决:
1、优化业务流程,减少对消息顺序的依赖(1图变成2图,先确认一下消费者订单最终状态,然后业务处理)
2、通过特定的路由策略确保相同订单号的消息发送到同一个partition,利用单个partition的有序性来保证消息顺序。
但如果真的有需要保证消息顺序的需求,可以将不同订单号路由到不同的partition,相同订单号的消息,每次都发到同一个partition,利用kafka的单个partition有序来保证顺序消费。
消息堆积:
1、消费者处理消息的速度小于生产者生成消息的速度
问题解决:
解决这个问题需要从提升消费者处理能力或者限制生产者生成消息的速度入手。
1、限制生产者生成消息的速度
2、增加更多消费者,提高消费速度
3、在消费者内开启线程池加快消息处理速度(如果不需要保证顺序)
4、扩大队列容积,提高堆积上限,采用惰性队列
若要保证顺序:
重复消费:
1、生产者产生了重复的消息
2、kafka或rocketmq的offset被回调了
3、消息消费失败(网络问题、系统故障)
4、业务系统主动发起重试
问题解决:
不管是由于生产者产生的重复消息,还是由于消费者导致的重复消费,我们都可以在消费者中解决这个问题。这就要求消费者在做业务处理时,要做幂等设计(Redis分布式锁,SQL乐观锁(版本号)等等)。推荐增加一张消费消息表【与消息丢失的消息发送表可以是同一张表】,来解决mq的这类问题。消费消息表中,使用messageId做唯一索引,在处理业务逻辑之前,先根据messageId查询一下该消息有没有处理过,如果已经处理过了则直接返回成功,如果没有处理过,则继续做业务处理。
延时及过期失效问题:在某些应用场景中,需要实现消息的延时处理或设置消息的过期时间。解决这个问题可以通过使用MQ提供的延时队列功能或通过业务逻辑处理过期失效的消息。
大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,晚上12点以后,用户都睡觉了。写个临时程序,将丢失的数据一点一点地查出来,重新灌入mq里。
数据一致性问题:
1、强一致性
2、弱一致性
3、最终一致性
问题解决
为了性能考虑大部分使用的是最终一致性,这就必定会出现数据不一致的问题。这类问题大概率是因为消费者读取消息后,业务逻辑处理失败导致的,可以增加重试机制解决。
重试分为:同步重试 和 异步重试。
有些消息量比较小的业务场景,可以采用同步重试,在消费消息时如果处理失败,立刻重试3-5次,如果还是失败,则写入到记录表中。但如果消息量比较大,则不建议使用这种方式,因为如果出现网络异常,可能会导致大量的消息不断重试,影响消息读取速度,造成消息堆积。
消息量比较大的业务场景,建议采用异步重试,在消费者处理失败之后,立刻写入重试表,有个job专门定时重试。
或者消费失败,自己给同一个topic发一条消息,在后面的某个时间点,自己又会消费到那条消息,起到了重试的效果。如果对消息顺序要求不高的场景,可以使用这种方式。
知识点
BlockingQueue
1、继承Queue接口
2、BlockingQueue 常用于生产者-消费者模型中,生产者线程会向队列中添加数据,而消费者线程会从队列中取出数据进行处理。Java线程池中用的比较多。
3、实现类
ArrayBlockingQueue
LinkedBlockingQueue
PriorityBlockingQueue
SynchronousQueue:同步队列,是一种不存储元素的阻塞队列。每个插入操作都必须等待对应的删除操作,反之删除操作也必须等待插入操作。
DelayQueue:延迟队列,其中的元素只有到了其指定的延迟时间,才能够从队列中出队。
4、ArrayBlockingQueue 和 LinkedBlockingQueue
底层实现:ArrayBlockingQueue 基于数组实现,而 LinkedBlockingQueue 基于链表实现。
是否有界:ArrayBlockingQueue 是有界队列,必须在创建时指定容量大小。LinkedBlockingQueue 创建时可以不指定容量大小,默认是Integer.MAX_VALUE,也就是无界的。但也可以指定队列大小,从而成为有界的。
锁是否分离: ArrayBlockingQueue中的锁是没有分离的,即生产和消费用的是同一个锁;LinkedBlockingQueue中的锁是分离的,即生产用的是putLock,消费是takeLock,这样可以防止生产者和消费者线程之间的锁争夺。
内存占用:ArrayBlockingQueue 需要提前分配数组内存,而 LinkedBlockingQueue 则是动态分配链表节点内存。这意味着,ArrayBlockingQueue 在创建时就会占用一定的内存空间,且往往申请的内存比实际所用的内存更大,而LinkedBlockingQueue 则是根据元素的增加而逐渐占用内存空间。