怎样效率的消费队列
队列简介
广义上只要是可以维持先进先出(FIFO)原则的数据存储方式都可以称为队列,小至基本的数据结构:array,list,queue等,大至各项软件服务比如带自增键的mysql表,MongoDB的collection,redis的list,又或常见的消息队列,ActiveMQ,RabbitMQ,Kafka等,更有第三方消息服务,AmazonSQS, AliyunMNS等。
在现代互联网高流量的架构中,消息队列一直占据着核心地位,多用在要求系统解耦、流量削峰、高并发缓冲、消息订阅、数据同步等强需求功能上。队列作为上下游的中间件,为下游缓冲数据的同时,也需要下游系统及时消费,以避免队列发生数据堆积,从而影响正常业务流程。
这篇文章,主要就是介绍下消息队列下游消费端要注意的一些事情,欢迎大家探讨。^_^
“安全的”消费
消费消息队列怎么样才能算是“安全的”呢?其最基本是要保证下面两点:
1. 不丢队列数据
2. 保证FIFO时序
怎么理解这两条,拿即时聊天系统(IM)的消息队列举例,就是既要保证不会漏掉某条的聊天记录,并且也要按时间先后顺序逐条显示。本文主要分享下,下游消费端的安全的消费,至于消息队列本身安全机制,这里就不在探讨。有兴趣的同学,可以去了解各种消息队列的连接池、QoS、去重等机制。
“效率的”消费
提升队列的消费效率,是每个消费端都要思考的东西。毕竟消息堆积,不仅仅是队列的负载变高,同时意味着下游的接受数据延迟变长,进而导致数据一致性、用户体验等一系列问题。
1. 优化下游消费进程
避免因为不合理的设计或开发导致的消费进程阻塞;比如第三方请求、复杂数据库处理或复杂cpu运算等场景就不适合放到消费进程中。
2. 批量处理
批量拉取消息,批量查询,批量写,提升整体吞吐率。
3. 并发多进程消费
一般情况,消息队列本身的吞吐量不是瓶颈(特大消息量的大佬们请无视.),下游消费端的消费速率才是瓶颈,在前两条不能在优化的情况下,并发是最常用的手段。
并发多进程的“安全消费”
大家都知道多进程执行的情况,因为执行环境和时间的不确定性,消息的时序没有办法保证,从而导致乱序。目前主要有两种解决方案:
1. 低粒度的时序
举个栗子,拿微信聊天来说,对于参与聊天的双方(即session)发送和收到的消息能保证时序就可以了,至于该聊天记录同其他聊天session之间的消息的FIFO先后关系根本无关紧要。这样的业务场景就使得我们在可以在session粒度上保证时序就可以了,而不需要考虑全局时序,这就是所谓的低粒度时序。
有了这个妥协的时序,就使得多进程消费有了可行性。我们可以将同一个聊天session的所有消息全部映射到同一个队列上,由单进程串行按时序消费该队列,从而保证同一session的时序性。同样在一个电子商务系统,同一个订单的下单-支付-库存-物流等串行逻辑,也可以依赖orderid的低粒度时序,来实现多线程消费。
2. 使用消息版本号
上游系统在生成消息时,同时添加该消息的版本号,使得下游多进程消费时,可以通过消息内的含有版本号来决定是否使用该消息。该方案适用于下游系统不关心过程数据,只关心数据结果的场景。
几种非常规的队列
自增mysql表
该队列的时序主要体现在自增键上,键值越小需先消费。多进程消费时,每个进程都会读取表中的每条数据,再通过hash映射来来决定是否在当前进程进行消费,不在当前进程映射的数据跳过。为了消费安全,可以记录每个进程当前消费的自增键,从而在进程异常退出的时候,也可以保证在下次重启时继续消费。至于hash映射,最常用算法就是键值取模,或者依赖低粒度时序字段取模。
MongoDb的collection
mongodb-collection有个自带的主键_id(ObjectID类型),ObjectID本身就是支持分布式时序的,(ObjectId的结构,有兴趣的同学可以看一下),故使用依赖该主键也可以实现队列。_id越小的记录就可以最先消费。至于多进程消费逻辑,可以参照 自增mysql表