Delay - 如何用 Redis 打造一个延迟队列、广播(设计概述)

如何用 Redis 打造一个延迟队列、广播(设计概述)

前言:

何为延迟队列?这个话题我相信在阅读本文的时候就已经有很明确的答案了,那么想要实现一个延迟队列,应该具备哪些条件,如何做到更加灵活,且拥有高扩展能力?下面开始一一分析


1. Redis 数据结构的选择

在此,大多数人都知道使用 Zset 可以做延迟队列,也有很多博客当中描述了相关的使用和设计,在这里我也会做详细的解释:

1.1. Zset 的使用(超时列表)

## 添加元素
Zadd key score member
# Zadd zyred 1 test

## 统计元素个数
ZCard key
# ZCard zyred  ->  1

## 通过 score 返回有序集合指定区间内的成员
ZRangeByScore key min max
# ZRangeByScore zyred 0 1		-> 0到1之间的所有元素

## 获取元素的 score 值
ZScore key member
# ZScore zyred test    ->  1

## 删除元素
ZRem key member
# ZRem zyred test

在上面介绍了 Zset 集合的命令,score 可以使用时间戳,而 member 则需要设计一下。如何设计一个合理的 member 则需要接下来的分析:
目前市面上的延迟队列都是使用 TOPIC 来表示消费者,那么在我们的延迟队列中 member 也可以做成 TOPIC,但是这样并不完美,为了更好的区分每一条消息,于是在消息体内设置了一个 messageId 的属性,那么只要将 TOPICmessageId 组合在一起,用一个特殊符号隔开,就能完美的设计出 member

number ->  TOPIC:messageId
score  ->  System.currentTimeMillis() + delay(消息多少ms过期)

1.2 Hash 的使用(元数据列表)

## 添加消息体到 hash
HSet key filed value
# HSet my_key zyred 1

## 获取消息体
HGet key field
# HGet my_key zyred

## 删除消息体
HDel key field
# HDel my_key zyred

定义: 为什么要使用 hash(首先 hashkey 是不能重复的,这个特性我们必须要清楚), 使用 hash , 主要的目的是为了存储元数据,所谓的元数据就是用户提交的消息体内容。然而为什么要这样设计,为什么不直接将元数据放入到 zsetmember 中,这样做的目的是为了让 zset 更好的维护,显得没有那么杂乱无章。

从上述中,能了解到 hashvalue 字段保存的是消息体,那么 key 应该如何设计。在 zset 中的 member 我们设计为 TOPIC:messageId 的格式,那么当 zset 中的消息过期后,是不是应该拿着这个 member 去找到对应的消息,如何更快的找到 hash 中的 value 交给用过去消费,无非就是使用 TOPIC:messageId 来保证 hash key 的唯一性

hash field 		->    TOPIC:messageId
hash value   	-> 	  {json}

1.3 Set 集合的使用 (待消费列表)

 ## 添加一个元素
 SAdd key member 
 # SAdd set_key zyred
 
 ## 批量获取元素
 SMembers key
 # SMembers set_key

## 删除单个
SRem key member
# SRem set_key zyred

Set 集合主要是针对消费者,当 set 集合中有消息待消费的情况,消费线程会主动从 set 集合中拿去消息来消费, member 的设计与 hashfiled 保持一致

set member -> TOPIC:messageId

1.4 数据结构使用总结

添加一条消息到延迟队列,此时会构建一个 fieldTOPIC:messageId, 并且将消息序列化为 json 保存到 hash 中,hash 内保存完毕后,将 field 与消息过期时间传入到 zset 内,如下图所示:

  • hash
    在这里插入图片描述
  • zset
    在这里插入图片描述
    当 zset 里的 field 过期后,会通过某种方式将 field 放入到 set 集合中并且通知消费者线程进行消费消息,最终消费者会从 hash 中拿到消息体从而进行消费。

2. 线程设计

通过 Redis 数据结构的选择 章节中能够大致的了解到数据在各个数据结构之间的扭转,那么清楚了底层的扭转逻辑就针对这个逻辑进行设计系统

2.1 线程模型

2.1.1 搬运线程

定义: 搬运线程即从 zset 中转移超时的消息到 set 集合中提供给消费线程使用,这个线程极为重要。

为什么要将搬运的动作设计为单线程?
如果将搬运做成了多线程,那么会出现一个问题,会不会出现重复搬运的情况? 这是必然会出现的,所以为了避免 field 被重复搬运,那么这里就做单线程搬运,从根源上杜绝了重复消费的问题

2.1.2 消费线程

定义: 消费线程与消费者个数绑定,有多少个消费者就会创建多少个线程,当然消费者成千上万个的时候,肯定不太适合使用 redis 来做延迟队列。

2.2 线程通讯

线程通讯主要是当搬运线程成功搬运一个或多个 filed 的时候,来唤醒指定 topic 的消费线程进行消费消息,这个比较抽象,不太好画图,索性通过一个场景来描述这个抽象的问题

一个班级有N个学生,一个班主任,此时学生都在睡午觉,而班主任会定时巡查是否有家长来找学生,突然班主任发现小明的妈妈来学校找小明,于是班主任将小明妈妈带到办公室,然后就跑到教室轻轻的唤醒小明(不会打扰其他学生),并告诉小明他的妈妈来找他。

在以上的场景中,学生是消费线程,班主任是搬运线程,而家长则是待消费的消息,班主任把家长带到办公室的动作就是搬运超时的消息,唤醒小明不打扰其他同学则是指定线程唤醒后消费消息,当小明妈妈与小明见面完毕后,小明会继续回到位置上睡午觉(别杠,这里只是一个场景)

3. 总结

通过本文对 Redis 做延迟队列的底层核心逻辑的剖析,能够知道采用的数据结构与线程,接下来的逻辑将会在下一篇文章中继续描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值