用Redis实现消息队列的四种思路

Redis实现消息队列

面试被问到redis实现消息队列的思路,参考学习了几篇博文的内容,整理如下。深入学习请参考文末链接或其他相关资料。

消息队列

首先,消息队列是应用之间异步通信的方式,主要由三个部分组成。

生产者,消息所承载业务信息的一个实例化,整个消息的发起方。

中间的broker是消息的服务端,主要是处理消息单元,负责消息的存储、投递等功能,是核心部分。

消费者,主要负责消息的消费,具体是根据消息承载的信息处理各种逻辑。

消息队列应用场景,主要分为三种。

1.异步处理,在实时性要求不严格的一些场景,比如用户注册发送验证码,下单通知发送优惠券等。服务方只需要把协商好的消息发送到消息队列,剩下的由消费者的消息服务去处理,不需要等待消费者的返回结果就可以返回给客户端,返回业务层面。

2.应用解耦。把一些相关但耦合性不高的系统关联起来,增加整个系统的灵活度。

3.流量削峰。为了权衡高可用,把大量并行任务发送到mq,根据mq的存储、分发功能平稳处理后续业务。起到大流量缓冲的作用。

消息队列需求

设计消息队列需要根据场景需求,通常考虑是否有如下三个需求

  • 消息保序
  • 处理重复消息
  • 保证消息可靠性

Redis实现消息队列

成熟的MQ有RocketMQ、Kafka、ActiveMQ等,为什么还需要Redis来自定义实现消息队列?

  • 有些简单的业务场景,不需要重量级的 MQ 组件, Redis 相对轻量易用。
基于List实现消息队列

List常用命令

命令用法描述
LPUSHLPUSH key value [value …]将一个或多个值 value 插入到列表 key 的表头如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表头
RPUSHRPUSH key value [value …]将一个或多个值 value 插入到列表 key 的表尾(最右边)
LPOPLPOP key移除并返回列表 key 的头元素。
BLPOPBLPOP key [key …] timeout移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
RPOPRPOP key移除并返回列表 key 的尾元素。
BRPOPBRPOP key [key …] timeout移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
BRPOPLPUSHBRPOPLPUSH source destination timeout从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它;如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
RPOPLPUSHRPOPLPUSH source destinationb命令 RPOPLPUSH 在一个原子时间内,执行以下两个动作:将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端。将 source 弹出的元素插入到列表 destination ,作为 destination 列表的的头元素
LLENLLEN key返回列表 key 的长度。如果 key 不存在,则 key 被解释为一个空列表,返回 0 .如果 key 不是列表类型,返回一个错误
LRANGELRANGE key start stop返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定
127.0.0.1:6379> lpush mylist a a b c d e 
(integer) 6 
127.0.0.1:6379> rpop mylist 
"a" 
127.0.0.1:6379> rpop mylist 
"a" 
127.0.0.1:6379> rpop mylist 
"b" 

在这里插入图片描述

存在问题

实时消费

LPUSH、RPOP 存在一个性能风险,生产者向队列插入数据的时候,List 不会主动通知消费者及时消费。程序需要不断轮询并判断是否为空再执行消费逻辑。

Redis 提供了 BLPOP、BRPOP 阻塞读取的命令,消费者在在读取队列没有数据的时候自动阻塞,直到有新的消息写入队列,才会继续读取新消息执行业务逻辑。

消息可靠性

List 队列中的消息一经发送出去,便从队列里删除。如果由于网络原因消费者没有收到消息,或者消费者在处理这条消息的过程中崩溃了,就再也无法还原出这条消息,缺少消息确认机制。

Redis 提供了 RPOPLPUSH、BRPOPLPUSH(阻塞)两个指令,含义是从 List 从读取消息的同时把这条消息复制到另一个 List 中(备份),并且是原子操作。

我们就可以在业务流程正确处理完成后再删除队列消息实现消息确认机制。如果在处理消息的时候宕机了,重启后再从备份 List 中读取消息处理。

127.0.0.1:6379> rpush myqueue one 
(integer) 1 
127.0.0.1:6379> rpush myqueue two 
(integer) 2 
127.0.0.1:6379> rpush myqueue three 
(integer) 3 
127.0.0.1:6379> rpoplpush myqueue queuebak 
"three" 
127.0.0.1:6379> lrange myqueue 0 -1 
1) "one" 
2) "two" 
127.0.0.1:6379> lrange queuebak 0 -1 
1) "three" 

在这里插入图片描述

基于发布、订阅模式

Redis 通过 PUBLISH 、 SUBSCRIBE 等命令实现了订阅与发布模式, 这个功能提供两种信息机制, 分别是订阅/发布到频道和订阅/发布到模式。

频道我们可以先理解为是个 Redis 的 key 值,模式可以理解为是一个类似正则匹配的 Key,只是个可以匹配给定模式的频道。这样就不需要显式的去订阅多个名称了,可以通过模式订阅这种方式,一次性关注多个频道。

详细过程可参考Redis消息队列的三种方案文章的相关内容

命令用法描述
PSUBSCRIBEPSUBSCRIBE pattern [pattern …]订阅一个或多个符合给定模式的频道
PUBSUBPUBSUB subcommand [argument [argument …]]查看订阅与发布系统状态
PUBLISHPUBLISH channel message将信息发送到指定的频道
PUNSUBSCRIBEPUNSUBSCRIBE [pattern [pattern …]]退订所有给定模式的频道
SUBSCRIBESUBSCRIBE channel [channel …]订阅给定的一个或多个频道的信息
UNSUBSCRIBEUNSUBSCRIBE [channel [channel …]]指退订给定的频道
基于Streams的消息队列
描述用法
添加消息到末尾,保证有序,可以自动生成唯一IDXADD key ID field value [field value …]
对流进行修剪,限制长度XTRIM key MAXLEN [~] count
删除消息XDEL key ID [ID …]
获取流包含的元素数量,即消息长度XLEN key
获取消息列表,会自动过滤已经删除的消息XRANGE key start end [COUNT count]
以阻塞或非阻塞方式获取消息列表XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key …] id [id …]
创建消费者组XGROUP [CREATE key groupname id-or-] [DESTROY key groupname] [DELCONSUMER key groupname consumername]
读取消费者组中的消息XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key …] ID [ID …]
将消息标记为"已处理"XACK key group ID [ID …]
为消费者组设置新的最后递送消息IDXGROUP SETID [CREATE key groupname id-or-] [DESTROY key groupname]
删除消费者XGROUP DELCONSUMER [CREATE key groupname id-or-] [DESTROY key groupname]
删除消费者组XGROUP DESTROY [CREATE key groupname id-or-] [DESTROY key groupname] [DEL
显示待处理消息的相关信息XPENDING key group [start end count] [consumer]
查看流和消费者组的相关信息XINFO [CONSUMERS key groupname] [GROUPS key] [STREAM key] [HELP]
打印流信息XINFO STREAM [CONSUMERS key groupname] [GROUPS key] [STREAM key] [HELP]

list 和 zset 实现的队列都没能很好的支持多消费组的场景,Streams 数据类型,是为 redis 设计的消息队列,能支持多消费组的场景。

Streams 提供了丰富的消息队列操作命令。

  • XADD:插入消息,保证有序,可以自动生成全局唯一 ID;
  • XREAD:用于读取消息,可以按 ID 读取数据;
  • XREADGROUP:按消费组形式读取消息;
  • XPENDING 和 XACK:XPENDING 命令可以用来查询每个消费组内所有消费者已读取但尚未确认的消息,而 XACK 命令用于向消息队列确认消息处理已完成。

Stream 提供了 xreadgroup 指令可以进行消费组的组内消费,需要提供消费组名称、消费者名称和起始消息 ID。它同 xread 一样,也可以阻塞等待新消息。读到新消息后,对应的消息 ID 就会进入消费者的 PEL(正在处理的消息) 结构里,客户端处理完毕后使用 xack 指令通知服务器,本条消息已经处理完毕,该消息 ID 就会从 PEL 中移除。

基于zset的消息队列

延时队列来处理的场景,比如支付订单的15分钟有效时间、物流订单30分钟同步一次最新状态等等。这种场景,你可以考虑使用 redis 的 zset 来实现,zset 结构有一个参数 score, 可以用来控制时延。

zset 是一个有序的结构,每次取第一个元素来判断能否处理,如果第一个元素都不能处理,说明之后的元素都没有到执行时间。

zset 实现队列的几个特点:

  • 消息的有序性?zset 结构本身是按照 score 有序的,因此从消息投递先后来看便是无序;你可以根据 score 参数值的大小来控制消息在队列的先后顺序
  • 重复消息处理消息可靠性保障与 list 结构实现的队列类似

具体代码和过程可参考redis消息队列,你还不敢用?

总结

redis 消息队列优点:

  • 方便,相信目前很多项目开发已引入 redis, 因此,运维、学习成本等就降低了。
  • 轻量级,开箱即用,也可以自己做一层简单封装。
  • 多样性,可根据需求选择底层队列结构。

业务上避免过度复用一个 Redis,既用它做缓存、做计算,还拿它做任务队列。redis 队列可以考虑在非核心业务中使用,比如电商会员积分、短信下发、支付倒计时甚至一些定时处理的订单状态等。

参考博文

https://www.51cto.com/article/699922.html

https://www.51cto.com/article/640335.html

https://juejin.cn/post/7094272373930590245

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 延时队列是指队列中的任务需要在一定延时后才能被执行,可以有效地解决某些任务需要延时处理的问题。Java Redis是一个基于Redis的Java客户端,它可以实现Java语言对Redis数据结构的操作,因此可以很方便地实现延时队列的功能。 Java Redis实现延时队列的主要思路是利用Redis的Sorted Set数据结构,将任务按照延时时间作为Score,任务内容作为Value,插入Sorted Set中,并设置过期时间,过期后将任务弹出并执行。具体实现步骤如下: 1. 创建一个Sorted Set,将任务插入其中,Score为任务的延时时间,Value为任务的内容。 2. 使用Redis的zremrangebyscore命令扫描Sorted Set中Score小于等于当前时间的任务并弹出,并将任务内容推送到执行队列中。 3. 设置延时任务的过期时间,过期时间为延时时间加当前时间,可以使用Redis的zadd命令添加任务时同时设置Score和过期时间。 4. 执行队列按顺序执行任务,任务执行完成后从执行队列中删除。 通过以上步骤,可以实现一个高效可靠的延时队列,可以优化系统任务调度、异步处理、消息通知等场景下的问题。 ### 回答2: Java Redis延时队列是一种常用的消息队列模式,在很多应用场景中都有应用。Java Redis延时队列通过将消息发送到Redis进行存储,在指定的时间后再将消息取出来进行处理。这个过程中,通过Redis的Sorted Set类型进行排序来保证队列中的消息有序。下面来详细介绍Java Redis延时队列实现方式。 一、Redis数据结构 Java Redis延时队列的关键在于Redis数据结构的设计。在Redis中,Sorted Set就是用来解决排序问题的。所以我们需要借助Sorted Set实现延时队列。具体来说,可以使用Redis中的zadd命令将消息发送到Sorted Set中,并按照时间顺序进行排序。Sorted Set中的元素包含值和权重,我们可以根据权重(即时间戳)来实现有序存储。 二、消息入队 消息的入队过程如下: 1. 获取消息的过期时间TTL。 2. 计算出当前的时间戳now。 3. 将消息写入到Sorted Set中,权重为now+TTL。 ```redis-cli> ZADD delay-queue (now + TTL) message``` 三、消息出队 消息出队过程如下: 1. 获取当前时间戳now。 2. 使用zrangebyscore命令从Sorted Set中获取所有权重小于等于now的元素,即过期的元素。 3. 遍历查询结果,对每个元素执行出队操作(移除元素)。 ```redis-cli> ZRANGEBYSCORE delay-queue -inf now``` 四、多线程处理 为避免在出队过程中同时处理多个过期元素时出现问题,可以使用多线程处理消息。Java的并发包中提供了Executor框架,这里可以使用ThreadPoolExecutor线程池。 五、消息重试 有时候由于网络波动等原因,在执行消息处理时可能会失败,所以需要将失败的消息重新入队。此时,可以加入重试机制,重新入队时TTL加上重试时间,即可实现延时次数的控制。 六、总结 Java Redis延时队列利用Redis的Sorted Set实现有序存储,使用多线程和重试机制解决了消息处理时的并发和失败问题,保障了消息的可靠性。在实际应用中,可以根据业务需求进行调优和扩展,如设置合理的时间间隔、增加监控和报警等。 ### 回答3: Java Redis实现延时队列可以分为以下几步: 1.将任务加入到延时队列中:首先需要将任务和对应的过期时间放入Redis的有序集合中,以过期时间为score值,任务为value。这样可以保证按照过期时间顺序进行排序,具有先进先出的特点。代码实现如下: ```java //添加任务到延时队列 public void addToDelayQueue(String taskId, long delayTime) { //计算过期时间 long expireTime = System.currentTimeMillis() + delayTime; //添加到有序集合中,score为过期时间 jedis.zadd(DELAY_QUEUE_KEY, expireTime, taskId); } ``` 2.从延时队列中取出任务:需要循环从有序集合中取出第一个score小于等于当前时间的任务,并将其从有序集合中删除。代码实现如下: ```java //获取延时队列中的任务 public void getFromDelayQueue() { while (true) { //获取第一个score小于等于当前时间的任务 Set<String> set = jedis.zrangeByScore(DELAY_QUEUE_KEY, 0, System.currentTimeMillis(), 0, 1); if (set == null || set.isEmpty()) { try { Thread.sleep(1000);//如果没有取到任务,则等待1秒 } catch (InterruptedException e) { e.printStackTrace(); } continue; } String taskId = set.iterator().next(); //删除任务 jedis.zrem(DELAY_QUEUE_KEY, taskId); //处理任务 handleTask(taskId); } } //处理任务 public void handleTask(String taskId) { //TODO: 根据taskId执行对应的任务 } ``` 3.使用Redis发布订阅机制以及线程池来处理任务:在处理任务时,可以使用Redis发布订阅机制将任务相关的信息发布到指定的频道,由相应的消费者线程池来进行任务处理,可以降低单线程处理任务的压力。代码实现如下: ```java //处理任务 public void handleTask(String taskId) { //通过发布订阅机制将任务信息发布到指定频道 jedis.publish(TASK_CHANNEL, taskId); } //消费者线程池处理任务 public void consumeTask() { JedisPubSub jedisPubSub = new JedisPubSub() { public void onMessage(String channel, String message) { //TODO: 根据message信息执行对应的任务 } }; jedis.subscribe(jedisPubSub, TASK_CHANNEL); executorService.execute(jedisPubSub::quit); } ``` 通过以上步骤,我们就可以实现Java Redis延时队列的功能,实现了任务的延迟执行。延时队列实现可以实现具有任务按照时间顺序执行,且任务可取消、可重试等特点,应用场景广泛。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值