基于Redis实现特殊的消息队列

本文将介绍一种基于Redis实现的消息队列(Redis message queue, RMQ),RMQ可以作为传统消息队列的互补选择,在传统消息队列没有涉及的场景中使用RMQ。

功能介绍

RMQ设计为一个二方库,可以帮助用户基于Redis快速实现消息队列的功能,RMQ消息队列具有消息合并、区分优先级、支持定时消息等特性。RMQ消息队列可以用于异步解耦、削峰填谷,支持亿级数据堆积。RMQ消息队列目前支持三种类型的消息,分别是RangeMergeMessage(区间重复合并消息)、PriorityMessage(优先级消息)、FixedTimeMessage(任意定时消息)。

  区间重复合并消息

RangeMergeMessage支持区间重复消息合并,发送消息时需要设置时间区间,消息延迟该时间区间长度后被消费,在该时间区间内如果发送重复的消息,重复消息将会被合并。如果消息在Redis服务端发生堆积,重复到来的消息依然会被合并处理。

该类型消息适用于消息重复率较高且希望重复消息合并处理的场景,对重复消息进行合并可以减少下游消费系统的压力,减少不必要的资源消耗,将有限的资源最大化的利用,提升消费效率。

  优先级消息

PriorityMessage支持给消息设置任意等级的优先级,优先级高的消息会被优先消费,相同优先级的消息被随机消费。如果消息在Redis服务端发生堆积,重复的消息将被合并处理,合并后消息的优先级等于最后存储的消息的优先级。

该类型消息适用于希望重复消息合并处理且需要设置优先级的场景,下游消费者资源有限时,合并重复消息且优先处理优先级高的消息将可以合理利用有限的资源。


  任意定时消息

FixedTimeMessage支持给消息设置任意消费时间,只有消费时间到了之后消息才被消费,消费时间可精确到秒。消息到期后没有及时被消费时,消费者将按照时间由远及近进行消费。如果消息在Redis服务端发生堆积,重复的消息将被合并处理,合并后消息的消费时间等于最后存储的消息的消费时间。

该类型消息适用于希望重复消息合并处理且需要定时消费的场景,定时消息应用场景非常丰富,比如定时打标去标、活动结束后清理动作、订单超时关闭等。

  并发消费控制

使用传统消息中间件进行集群消费的时候,为了避免并发处理同一元数据导致不一致问题,通常需要对元数据加分布式锁,频繁的锁冲突会导致消费效率低下。加分布式锁的最终目的其实就是保障属于同一元数据的消息被串行消费。加分布式锁并不是最好的方案,最好的方案应该是从根上解决并发问题,让属于同一元数据的消息串行消费。

RMQ消息队列具有并发消费控制能力,属于同一元数据的消息只会被分配给全局唯一一个线程进行消费,因此属于同一元数据的消息将被串行消费。使用方如果需要该能力,除了需要提供Redis,还需要提供ZooKeeper。

  重试次数控制

RMQ消息队列支持失败重试消费16次,业务返回消费失败后,消息会被回滚并等待重试消费,重试16次后消息进入死信队列,消息不再被消费,除非人工干预。

技术原理


  总体框架

RMQ消息队列由三部分组成,分别为ZooKeeper、RMQ二方库、Redis。ZooKeeper负责维护集群worker的信息,将topic的所有slot分配给全局的woker。Redis负责存储消息,采用Sorted Set结构存储,Store Queue是消息存放的队列,Prepare Queue是采用二阶段消费方式正在消费的消息存放队列,Dead Queue是死信队列。RMQ二方库由RmqClient、Consumer、Producer三部分组成。RmqClient负责RMQ的启动工作,包括上报TopicDef、Worker给ZooKeeper,分配Slot给Worker,扫描业务定义的MessageListener Bean。Producer负责根据不用消息类型将消息按照指定的方式存储到Redis。Consumer负责根据不用消息类型按照指定方式从Redis弹出消息并调用业务的MessageListener。

  消息存储

  • Topic的设计

Topic的定义有三部分组成,topic表示主题名称,slotAmount表示消息存储划分的槽数量,topicType表示消息的类型。主题名称是一个Topic的唯一标示,相同主题名称Topic的slotAmount和topicType一定是一样的。

消息存储采用Redis的Sorted Set结构,为了支持大量消息的堆积,需要把消息分散存储到很多个槽中,slotAmount表示该Topic消息存储共使用的槽数量,槽数量一定需要是2的n次幂。在消息存储的时候,采用对指定数据或者消息体哈希求余得到槽位置。

  • StoreQueue的设计

上图中topic划分了8个槽位,编号0-7。如果发送方指定了消息的slotBasis,则计算slotBasis的CRC32值,CRC32值对槽数量进行取模得到槽序号,SlotKey设计为#{topic}_#{index}(也即Redis的键),其中#{}表示占位符。

发送方需要保证相同内容的消息的slotBasis相同,如果没有指定slotBasis则采用消息内容计算SlotKey,这样内容相同的消息体就会落在同一个Sorted Set里面,所以内容相同的消息会进行合并。

Redis的Sorted Set中的数据按照分数排序,实现不同类型的消息的关键就在于如何利用分数、如何添加消息到Sorted Set、如何从Sorted Set中弹出消息。优先级消息将优先级作为分数,消费时每次弹出分数最大的消息。任意定时消息将时间戳作为分数,消费时每次弹出分数大于当前时间戳的一个消息。

区间重复合并消息将时间戳作为分数,添加消息时将(当前时间戳+时间区间)作为分数,消费时每次弹出分数大于当前时间戳的一个消息。

  • PrepareQueue的设计

为了保障RMQ消息队列的可用性,做到每条消息至少消费一次,消费者不是直接pop有序集合中的元素,而是将元素从StoreQueue移动到PrepareQueue并返回消息给消费者,等消费成功后再从PrepareQueue从删除,或者消费失败后从PreapreQueue重新移动到StoreQueue,这便是根据二阶段提交的思想实现的二阶段消费。

在后面将会详细介绍二阶段消费的实现思路,这里重点介绍下PrepareQueue的存储设计。StoreQueue中每一个Slot对应PrepareQueue中的Slot,PrepareQueue的SlotKey设计为prepare{#{topic}#{index}}。PrepareQueue采用Sorted Set作为存储,消息移动到PrepareQueue时刻对应的(秒级时间戳*1000+重试次数)作为分数,字符串存储的是消息体内容。这里分数的设计与重试次数的设计密切相关,所以在重试次数设计章节详细介绍。

PrepareQueue的SlotKey设计中需要注意的一点,由于消息从StoreQueue移动到PrepareQueue是通过Lua脚本操作的,因此需要保证Lua脚本操作的Slot在同一个Redis节点上,如何保证PrepareQueue的SlotKey和对应的StoreQueue的SlotKey被hash到同一个Redis槽中呢。Redis的hash tag功能可以指定SlotKey中只有某一部分参与计算hash,这一部分采用{}包括,因此PrepareQueue的SlotKey中采用{}包括了StoreQueue的SlotKey。

  • DeadQueue的设计

消息重试消费16次后,消息将进入DeadQueue。DeadQueue的SlotKey设计为prepare{#{topic}#{index}},这里同样采用hash tag功能保证DeadQueue的SlotKey与对应StoreQueue的SlotKey存储在同一Redis节点。

  生产者

生产者的任务就是将消息添加到Redis的Sorted Set中。首先,需要计算出消息添加到Redis的SlotKey,如果发送方指定了消息的slotBasis(否则采用content代替),则计算slotBasis的CRC32值,CRC32值对槽数量进行取模得到槽序号,SlotKey设计为#{topic}_#{index},其中#{}表示占位符。然后,不同类型的消息有不同的添加方式,因此分布讲述三种类型消息的添加过程。

  • 区间重复合并消息

发送该消息时需要设置timeRange,timeRange必须大于0,单位为毫秒,表示消息将延迟timeRange毫秒后被消费,期间到来的重复消息将被合并,合并后的消息依然维持原来的消费时间。

因此在存储该类型消息的时候,采用(当前时间戳+timeRange)作为分数,添加消息采用Lua脚本执行,保证操作的原子性,Lua脚本首先采用zscore命令检查消息是否已经存在,如果已经存在则直接返回,如果不存在则执行zadd命令添加。

  • 优先级消息

发送该消息时需要设置priority,priority必须大于16,表示消息的优先级,数值越大表示优先级越高。因此在存储该类型消息的时候,采用priority作为分数,采用zadd命令直接添加。

  • 任意定时消息

发送该类型消息时需要设置fixedTime,fixedTime必须大于当前时间,表示消费时间戳,当前时间大于该消费时间戳的时候,消息才会被消费。因此在存储该类型消息的时候,采用fixedTime作为分数,采用命令zadd直接添加。

  消费者

  • 二阶段消费方式

三种消费模式

一般消息队列存在三种消费模式,分别是:最多消费一次、至少消费一次、只消费一次。最多消费一次模式消息可能丢失,一般不怎么使用。至少消费一次模式消息不会丢失,但是可能存在重复消费,比较常用。只消费一次模式消息被精确只消费一次,实现较困难,一般需要业务记录幂等ID来实现。RMQ实现了至少消费一次的模式,那么如何保证消息至少被消费一次呢?

至少消费一次模式实现的难点

从最简单的消费模式——最多消费一次说起,消费者端只需要从消息队列服务中取出消息就行,即执行Redis的zpopmax命令,不伦消费者是否接收到该消息并成功消费,消息队列服务都认为消息消费成功。最多一次消费模式导致消息丢失的因素可能有:网络丢包导致消费者没有接收到消息,消费者接收到消息但在消费的时候宕机了,消费者接收到消息但消费失败。针对消费失败导致消息丢失的情况比较好解决,只需要把消费失败的消息重新放入消息队列服务就行,但是网络丢包和消费系统异常导致的消息丢失问题不好解决。

可能有人会想到,我们不把元素从有序集合中pop出来,我们先查询优先级最高的元素,然后消费,再删除消费成功的元素,但是这样消息服务队列就变成了同步阻塞队列,性能会很差。

至少消费一次模式的实现

至少消费一次的问题比较类似银行转账问题,A向B账户转账100元,如何保障A账户扣减100同时B账户增加100,因此我们可以想到二阶段提交的思想。第一个准备阶段,A、B分别进行资源冻结并持久化undo和redo日志,A、B分别告诉协调者已经准备好;第二个提交阶段,协调者告诉A、B进行提交,A、B分别提交事务。RMQ基于二阶段提交的思想来实现至少消费一次的模式。

RMQ存储设计中PrepareQueue的作用就是用来冻结资源并记录事务日志,消费者端即是参与者也是协调者。第一个准备阶段,消费者端通过执行Lua脚本从StoreQueue中Pop消息并存储到PrepareQueue,同时消息传输到消费者端,消费者端消费该消息;第二个提交阶段,消费者端根据消费结果是否成功协调消息队列服务是提交还是回滚,如果消费成功则提交事务,该消息从PrepareQueue中删除,如果消费失败则回滚事务,消费者端将该消息从PrepareQueue移动到StoreQueue,如果因为各种异常导致PrepareQueue中消息滞留超时,超时后将自动执行回滚操作。二阶段消费的流程图如下所示。

实现方案的异常情况分析

我们来分析下采用二阶段消费方案可能存在的异常情况,从以下分析来看二阶段消费方案可以保障消息至少被消费一次。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

小编精心为大家准备了一手资料

以上Java高级架构资料、源码、笔记、视频。Dubbo、Redis、设计模式、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术

【附】架构书籍

  1. BAT面试的20道高频数据库问题解析
  2. Java面试宝典
  3. Netty实战
  4. 算法

BATJ面试要点及Java架构师进阶资料

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
y、zookeeper、Spring cloud、分布式、高并发等架构技术

【附】架构书籍

  1. BAT面试的20道高频数据库问题解析
  2. Java面试宝典
  3. Netty实战
  4. 算法

[外链图片转存中…(img-3zTokOG5-1712419447982)]

BATJ面试要点及Java架构师进阶资料

[外链图片转存中…(img-67INC0Z8-1712419447982)]

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

  • 9
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值