基于Redis的延迟队列

业务背景

7号10:00创建活动,7号0:00到7号10:00之间下单未支付的用户push催付,7号10:00后用户下单5分钟内没有支付,以PUSH的方式催付

方案

0-10点间数据可理解为离线数据,可通过创建一个ad-hoc查询任务,并吐出人群到给发送服务。

下单后5分钟后没有支付以push催付,采用Redis的延迟列队方案

如果为下单半个小时后催付,建议采用DB轮询方案。

https://blog.csdn.net/asd491310/article/details/90418292中列举了延迟任务的解决方案,不论是RocketMQ、TimingWheel等方案,基本思路是有效减少扫描数据集。

集合S按时间排序,T1轮询队头若干个元素,判断是否符合条件,若符合条件,就取出所有符合条件的元素。

假设每5分钟符合预期的元素有1000个以上,建议通过消息中间件发送给专门消息上发送的集群处理

假设每5分钟符合预期的元素在1000以内,建议采用Reactor线程池模式。

方案思考:

优点:实现简单,轮询消耗资源少

缺点:按延迟时间建立延迟队列,建议按1、5、10分钟做延迟队列,不建义随意创建延迟队列,不好维护,一个队列最大吞吐量为单台Redis的吞吐量。

数据结构

假设每5分钟内的订单数据为10000以内,5秒轮询一次

a. Sorted sets,timestamp为score

1. 查找元素是否到达可发送时间:

    ZREMRANGEBYSCORE key min max

    时间复杂度: O( log(N)+ M),其中 N 是已排序集合中元素的数量,M 是由操作移除的元素数量,复杂度为1004

    注意:这里不使用ZCOUNT key min max  

    时间复杂度: O( log(N)),其中 N 是有序集合中元素的数量。复杂度为4。当ZREMRANGEBYSCORE 没有返回数据时时间复杂度均为O( log(N)),同ZREMRANGEBYSCORE 可简化应用代码逻辑,查找、获取数据、删除数据代码统一了。也可取一个元素存储在本地做轮询。减少轮询对Redis的开销

2. 删除已移除购物车的元素

    ZREM key member [member ...]

    时间复杂度: O( M * log(N)),其中 N 是有序集合中元素的数量,M 是要移除的元素的数量,复杂度为4*1

3. 新增元素

    ZADD key [NX|XX] [CH] [INCR] score member [score member ...]

    时间复杂度:添加每个项目的O(log(N)),其中N是排序集合中元素的数量,复杂度为4

   延迟时间内数据量10000以内,Sorted sets为笔者建议数据结构,数据量在往上建议,查找元素步骤与获取数据步骤分离,减少对Redis的压力。

b. List ,按插入顺序存储

假设秒级误差可以忽略,那么从5秒级的视角看,List也是一个有效有序的列表,且符合业务预期

1. 查找元素是否到达可发送时间:

 LINDEX key index

   时间复杂度: O(N)其中N是要到达索引处元素的遍历元素的数量。

   LINDEX key 0 查询列表第一个元素,复杂度为1

2. 删除已移除购物车元素

    LREM key count value,时间复杂度为O(N),不建议使用。移除购物车的订单建议使用string结构存储,有效期为10分钟。

不支持

3. 新增元素

    lpush/rpush,复杂度为1

4. 获取元素

     lpop/rpop,复杂度为1

### Redis 实现延迟队列教程及最佳实践 #### 一、基本概念 Redis 是一种高性能的键值存储数据库,支持多种数据结构操作。其中 `ZSet` 数据结构因其有序性和高效性被广泛用于实现延迟队列延迟队列的核心思想是将任务按照执行时间存入一个集合中,在到达指定的时间后再取出并处理。 通过使用 Redis 的 `ZSet` 和阻塞命令(如 `ZRANGE`, `ZPOPMIN` 或者 `BLPOP`),可以构建一个高效的延迟队列系统[^1]。 --- #### 二、实现方法 ##### 方法一:基于 ZSet 的延迟队列 以下是利用 Redis 中的 `ZSet` 来实现延迟队列的具体过程: 1. **任务存储** 将任务 ID 存储到 `ZSet` 中,分数设置为任务的触发时间戳(单位通常为秒)。例如: ```bash ZADD delay_queue <timestamp> task_id_001 ``` 2. **任务消费** 使用 `ZPOPMIN` 命令获取当前时间之前的任务。如果需要更高的并发能力,则可以通过多线程或多进程的方式调用该命令。 ```bash ZPOPMIN delay_queue ``` 3. **定时轮询机制** 如果不希望频繁轮询 Redis,可以选择配合 Lua 脚本或者后台服务来减少不必要的查询开销。 此方法的优点在于简单易懂且灵活性高,缺点是在大规模场景下可能面临性能瓶颈[^2]。 --- ##### 方法二:结合 Spring Boot 的解决方案 对于 Java 开发人员来说,借助 Spring Boot 可以快速搭建起一套完整的延迟队列框架。下面是一个典型的例子说明如何集成 Redis 并完成相关功能开发: 1. 配置 Redis 连接池参数; 2. 创建自定义注解标记哪些方法属于异步任务处理器; 3. 编写工具类负责向 Redis 插入待办事项以及监听到期事件; 更多细节可参考文章《Spring Boot + Redis 实现延时队列》中的描述[^3]。 --- #### 三、优化建议 为了提升系统的稳定性和效率,可以从以下几个方面入手进行改进: - **分片策略**:当单机内存不足以支撑海量数据时,考虑引入分区算法把不同类型的业务分开管理。 - **持久化配置**:合理调整 RDB/AOF 文件生成频率避免因断电丢失重要信息。 - **监控报警体系建立**:实时跟踪各项指标变化趋势以便及时发现问题所在。 另外值得注意的是,虽然上述提到的方法已经能够满足大部分应用场景的需求,但在极端情况下仍需进一步探索其他替代品比如 RabbitMQ Delayed Message Plugin 等专门针对此类问题设计的消息中间件产品[^4]。 --- ```python import time from redis import StrictRedis def add_task(redis_client, queue_name, task_id, delay_seconds): """ 添加新任务 """ execute_time = int(time.time()) + delay_seconds redis_client.zadd(queue_name, {task_id: execute_time}) def consume_tasks(redis_client, queue_name): """ 消费过期任务 """ while True: current_timestamp = int(time.time()) expired_tasks = redis_client.zrangebyscore(queue_name, '-inf', current_timestamp) if not expired_tasks: break for task in expired_tasks: process_task(task.decode('utf-8')) redis_client.zremrangebyscore(queue_name, '-inf', current_timestamp) def process_task(task_id): print(f"Processing Task-{task_id}") if __name__ == "__main__": rds = StrictRedis(host='localhost', port=6379, db=0) add_task(rds, 'delay_queue', 'job1', 5) time.sleep(6) # Simulate waiting period. consume_tasks(rds, 'delay_queue') ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值