消息队列就像是一个暂时存储数据的一个容器,是一个平衡低速系统和高速系统处理任务时间差的工具。
一,消息队列的作用
1.削峰
如果短时间之内数据库的写流量很高,那么正常思路是对数据做分库分表,如果已经做了分库分表,就需要扩展更多的数据库来应对更高的写流量。但是无论是分库分表,还是扩充更多的数据库,都会比较复杂,原因是需要将数据库中的数据做迁移,这个时间就要按天甚至按周来计算了。
如果是秒杀场景下,高并发的写请求并不是持续的,也不是经常发生的,而只有在秒杀活动开始后的几秒或者十几秒时间内才会存在。为了应对这十几秒的瞬间写高峰,就要花费几天甚至几周的时间来扩容数据库,再在秒杀之后花费几天的时间来做缩容,这无疑是得不偿失的。
所以:应该将秒杀请求暂存在消息队列中,然后业务服务器会响应用户“秒杀结果正在计算中”,释放了系统资源之后再处理其它用户的请求。
在后台启动若干个队列处理程序,消费消息队列中的消息,再执行校验库存、下单等逻辑。因为只有有限个队列处理线程在执行,所以落入后端数据库上的并发请求是有限的。而请求是可以在消息队列中被短暂地堆积,当库存被消耗完之后,消息队列中堆积的请求就可以被丢弃了。
2.通过异步简化业务流程
把主要的业务流程与次要的业务流程分开,放在不同的对列中,不仅可以简化流程还能进一步提高系统性能。
3.解耦实现系统模块之间松耦合
本系统与其他系统有数据同步需求时,有新数据产生时,可以先把全部数据发送给消息队列,然后其他系统再订阅这个消息队列的话题,这样大大降低了系统间耦合度,不至于一个系统故障或变更影响其他系统。
总的来说
1.削峰填谷是消息队列最主要的作用,但是会造成请求处理的延迟。
2.异步处理是提升系统性能的神器,但是要分清同步流程和异步流程的边界,同时消息存在着丢失的风险,需要考虑如何确保消息一定到达。
3.解耦合可以提升整体系统的鲁棒性。
二,消息丢失的问题
消息丢失的三个场景:
1.消息从生产者写入到消息队列的过程。
2.消息在消息队列中的存储场景。
3.消息被消费者消费的过程。
1. 在消息生产的过程中丢失消息
消息的生产者——业务服务器与消息队列服务器两者之间的网络发生抖动,消息就有可能因为网络的错误而丢失。
针对这种情况,建议采用的方案是消息重传来解决。
不过,这种方案可能会造成消息的重复,从而导致在消费的时候会重复消费同样的消息。
2. 在消息队列中丢失消息
拿 Kafka 举例,消息在 Kafka 中是存储在本地磁盘上的,而为了减少消息存储时对磁盘的随机 I/O,一般会将消息先写入到操作系统的 Page Cache 中,然后再找合适的时机刷新到磁盘上。Kafka 可以配置当达到某一时间间隔,或者累积一定的消息数量的时候再刷盘,也就是所说的异步刷盘。如果发生机器掉电或者机器异常重启,那么 Page Cache 中还没有来得及刷盘的消息就会丢失了。
3. 消费的过程中也存在消息丢失的可能
一个消费者消费消息的进度是记录在消息队列集群中的,而消费的过程分为三步:接收消息、处理消息、更新消费进度。
这里面接收消息和处理消息的过程都可能会发生异常或者失败,比如说,消息接收时网络发生抖动,导致消息并没有被正确的接收到;处理消息时可能发生一些业务的异常导致处理流程未执行完成,这时如果更新消费进度,那么这条失败的消息就永远不会被处理了,也可以认为是丢失了。
三,如何只消费一次
显然,为了避免消息丢失,我们需要付出两方面的代价:一方面是性能的损耗;一方面可能造成消息重复消费。
那么如何保证消息只被消费一次?
想要完全的避免消息重复的发生是很难做到的,因为网络的抖动、机器的宕机和处理的异常都是比较难以避免的,因此只要保证即使消费到了重复的消息,从最终结果来看和只消费一次是等同的就好了,也就是保证在消息的生产和消费的过程是“幂等”(一件事儿无论做多少次都和做一次产生的结果是一样的)的。
唯一ID的方式
实现幂等有一种较好的方法是为每一个消息生成一个唯一的 ID,然后在使用这个消息的时候,先比对这个 ID 是否已经存在,如果存在,则认为消息已经被使用过。
乐观锁的方式
除此之外,还可以在业务层面来保证消息只消费一次,比如通过乐观锁的方式来解决。
具体的操作方式是这样的:在数据中增加一个版本号的字段,在生产消息时先查询这条数据的版本号,并且将版本号连同消息一起发送给消息队列。消费端在拿到消息和版本号后,在执行消费的程序的时候带上版本号,如果版本号已经被更新了则无法消费成功。
四,关于消费的性能
1.我们可以使用消息队列提供的工具,或者通过发送监控消息的方式,来监控消息的延迟情况;
2.横向扩展消费者是提升消费处理能力的重要方式;
3.选择高性能的数据存储方式,配合零拷贝技术,可以提升消息的消费性能。
网友评论