消息的传递常系统规设计中都依赖MQ,目前MQ中间件常用的有RabbitMq、roketmq、kafka、zeromq、activeMq,咱们这里面先忽略mq的顺序性保障 ,重点讨论mq的上下游(producer、consumer)的消息顺序性保障如何实现。
如果这么设想的话,问题会变得简单些。 假定消息中间件的消息顺序性是可以保障的(消息中间件假定kafka: 一个topic下可以有多个partition, partition内的顺序可保障,消息可以多次消费)
首先来看一下 consumer的消息顺序性保障
1) 如果是一个topic 一个partition,partition中消息本来就是顺序的, 下游consumer的消费业务时延【可忽略】:一个线程就能搞定,此时是最简单的一种情况,直接单线程消费就能保障消息的时序性。
2) 如果是一个topic 一个partition,partition中消息本来就是顺序的,下游consumer的消费业务时延【不可忽略】:此时一个线程消费不过来了,容易照成业务堆积了。此时如何保障消息的顺序性?
假定业务场景:消息中存储的是用户的积分变动,且用户间消息无业务依赖。消息中有userid,此时用userid做hash,拆分成多个桶,每个桶对应一个线程进行处理此时即可保障单用户消息的时序性,利用业务特性提升consumer的并发消费能力。
具体操作:consumer启动x个线程(或进程)每个线程都消费当前partition所有的消息,如果这个消息的 userid%x=自己的序号就处理这条消息,否则就跳过(这里面可以进一步用一致性hash考虑consumer的可靠性)。
此场景再次扩展,如果某个hash下面存在热点如何处理: 如果业务特性允许可以进行二次hash。
看一下MQ的顺序性保障
1)刚才假定consumer只消费一个partition,如果当前mq的吞吐量不够如何处理
基于以上讨论mq单个partition的顺序性是能够保障的。此时可以考虑刚才consumer的处理方式,根据业务独立特性将消息进行分桶,不同的消息(或者按照某维度进行拆分)进入不同的partition。
此时高吞吐量的场景下:在mq以及下游consumer的消息的时序和时延都能够得到很好的保障。
此时利用各种hash将消息进行拆分,利用并发提高整体消息系统在mq和consumer的消息处理能力。
但是有些业务的对时序和时延的要求是不一样的以上方案只能解决部分场景下的问题。
producer的顺序性保障
如果源头消息的产生就不是顺序的下游如何处理都不不靠谱(此处太绝对)。因此消息源头如何保障顺序性至关重要。但是细想这个问题好像比上面的两个环节要更复杂。
1) 第一种情况 一个消息源头: 此时producer 是一个线程,线程内处理业务是顺序的产生的消息是本身自带顺序性,直接投放到MQ是没有问题的,hash投放到多个partition也是可以的,此时顺序是可以保证的。
2)第二种情况,多个消息源头: 此时producer 是多个线程。 单个线程内处理消息产出的顺序是顺序的,多个线程间的消息产出顺序不可保证。
例如:一个A线程修改一条数据,产生一条消息。 另外一个线程B修改同一条数据产生一条消息。 在全局时间空间里面,A发生在B前面。但是没法保证A消息一定先于B消息投放到MQ的同一个partition。这个场景就比较复杂了,中间实现的各种细节如果再考虑到这个问题就更加复杂了。
这个场景如何处理:
A)假设:消息产生都是对mysql数据库具体数据的变更,此时将mysql的binlog订阅到MQ里面即可。这个方法利用了将多源变成了单源,这种场景只是特例,系统中存在组件能够提供顺序性的消息刚好又和消息相关。
B)假设:消息的产生就是在业务层应用内的具体线程里面产出:此时再加个假设条件消息在具体线程里面产生时带的顺序标记(当前时间)是靠谱的。
有些场景是不靠谱的这个地方咱们简单说一下:如果触发某个函数会产生一条消息,函数完成了线程被挂起了,挂起5分分钟,当线程继续运行的时候获取当前时间作为顺序标识,完成消息的制造,此时顺序标记就是不靠谱的(为了能更好的抽象思考这个问题这个场景咱们先忽略)
此时我们已经有了线程内的具体时序标识,只要按照这个标识的先后顺序投递到MQ下游的消息顺序性就能保证了。但是保证先后顺序是很困难的,为什么这么说呢。按照目前MQ中间件提供的能力,其困难点主要在于有序投递,而有序投递目前MQ的中间件是不负责的。
此时假定我们有n个线程能够产生具有可靠顺序标记的消息,然后消息发送到某个消息排序缓存中,然后顺序的投递到MQ好像就能解决这个难题。但是得有这样一个消息排序缓存。直接想比较难,但是如果是个时序优先级的队列就好思考了,但是这个队列缓存未排好序的不能读。如下图:
以上讨论假定了很多因素(系统间的交互幂等,异常)只是为了聚焦时序本身。当然以上设计也是有很多问题的,假如某个app的消息发送不到缓冲区,为了保证时序性。我们是暂停消费还是丢弃(能保证时序)直接继续向下(保证时延小),还是直接下游消费等待上游app恢复后继续(消费者是否能接受这个延迟)。
看场景处理问题
两个名词(时序,时延),四个象限:
时序:代表消息的顺序性保障。
时延:代表消息从产生到被消费的时间间隔。
这里面具体的场景具体分析,但是同时保证这两项非常困难。以上场景的诸多假设把问题抽象简单了,但是实际场景中producer本来就有n个,而且一个producer又可能有n个线程。线程间的顺序要保证,然后所有producer的顺序要保证还是非常有挑战的。
当然,未来这些都能解决。