关于MQ队列如何保障顺序性消费的思考方案

526 阅读4分钟

写在前面

     作者一直想深入的去研究一款消息队列MQ,例如 延迟消费,广播消息,失败重试等机制.
 奈何市面上的MQ中间件太多了,如kafka,activeMQ,rocketMQ....所以也一直在纠结该以哪款为入手点,对新人友好?
 想起作者曾深入的研究了xuxueli大佬的分布式调度系统(XXL-JOB),然后进入XXL社区,刚好发现了的另一个开源项目,即分布式消息队列(XXL-MQ).       
   
   简而言之四个字,以小见大,懂得都懂

何为顺序性消费?

这个词经常出现在面试环节中,这里描述一下;
例如你往消息队列依次顺序地发送了以下消息

insert into student values (1,"张三")  //增加一条数据
update student name="张三名字修改" where id=1 //修改这条数据
delete from student where id=1   //删除这条数据

那么对于消费端来说,无论是集群还是单机部署,你希望看到的消息消费顺序也一定是这样
insert into student values (1,"张三")  //增加一条数据
update student name="张三名字修改" where id=1 //修改这条数据
delete from student where id=1   //删除这条数据

如果你的消费端消费顺序变成了
delete from student where id=1   //删除这条数据
update student name="张三名字修改" where id=1 //修改这条数据
insert into student values (1,"张三")  //增加一条数据

或者是
update student name="张三名字修改" where id=1 //修改这条数据
delete from student where id=1   //删除这条数据
insert into student values (1,"张三")  //增加一条数据

你会发现你的数据库多了一条脏数据,然后就等着卷铺盖滚蛋了

非顺序性消费 场景复现

这里给大家真实代码展示一下,非顺序性消费的情况
如图: 我部署了三个消费者集群 8005,8006,8007
然后模拟各个消费端的消费所用的不同时间
并且也是输出了当前消费者消费此条消息的时间
// 模拟网络卡顿等因素
Thread.sleep((int) (Math.random() * 20000));

wecom-temp-0091b512f80da205f73b13453c7b669e.png

然后我们往topic=topic_1 依次发送三条消息,
顺序性消费No:0
顺序性消费No:1
顺序性消费No:2

然后我们观察下8005,8006,8007三个消费端的消费情况

wecom-temp-a162da5a2f0e90f1ecd46c076a53a3bc.png

整理一下
8006 ,消费一条消息:顺序性消费No:1,消费时间Mon Feb 28 15:35:31 CST 2022
8005 ,消费一条消息:顺序性消费No:2,消费时间Mon Feb 28 15:35:41 CST 2022
8007 ,消费一条消息:顺序性消费No:0,消费时间Mon Feb 28 15:35:54 CST 2022

这里就发现了 消费顺序变成了顺序性消费No:1,顺序性消费No:2,顺序性消费No:0 即非顺序性

解决方案

  • 单机部署的消费端

    这种直接无需操作,自身即是顺序性,因为消息队列本身的特性就是先进先出,有顺序性的,单机部署消费去消息队列中心拉取消息时,也是按照顺序来的,如图(还是上面的例子)      
    

wecom-temp-37acc5380a0b237bb1699c72fe7ea007.png

  • 集群部署的消费端

    数据库乐观锁形式
    给每条消息增加扩展字段consumer_status,用于记录,维护每一条消息的消费状态
    如有以下顺序消息
     insert into student values (1,"张三")  //增加一条数据
     update student name="张三名字修改" where id=1 //修改这条数据
     delete from student where id=1   //删除这条数据
     
    当 8006消费者2准备MQ中心拉取消息 update student name="张三名字修改" where id=1 //修改这条数据  
    先查看前一条消息insert into student values (1,"张三")  //增加一条数据
    的consumer_status字段是否为 已消费结束/消费成功,如果是则拉取成功,并将这条消息字段更新为消费中,当8006消费者2 完成消息消费时,再将consumer_status字段更新成已消费结束/消费成功
    这期间对于8007消费者3来说,是一直被阻塞的,当8006完成消费后,8007才能继续消费,从而维持顺序性
    
    
    方案弊端:
    1 如果有10条顺序性消息,那么第一条消息没有被消费完成时,则会一直阻塞后续消息的消费,造成网络拥堵
    2 消息还有个失败重试机制,即消息被消费失败后,可以有几次重试消费机会,如何去合理管理消息失败重试阶段与消息消费状态consumer_status的维护,这个二次成本应该有点大
    
    如何优化?
    做成topic 业务可配置选项,例如在银行,支付等业务中,这种顺序性消费就如同刚性分布式事务一样,和💰💰💰有关,因此这种业务一定要开启此配置
    其他的例如网络群聊,广告推送这种要求不严的业务,则不开启此配置
    

End