基于Redis的简易延时队列
基于Redis的简易延时队列
一、背景
在实际的业务场景中,经常会遇到需要延时处理的业务,比如订单超时未支付,需要取消订单,或者是用户注册后,需要在一段时间内激活账号,否则账号失效等等。这些业务场景都可以通过延时队列来实现。
最近在实际业务当中就遇到了这样的一个场景,需要实现一个延时队列,用来处理订单超时未支付的业务。在网上找了一些资料,发现大部分都是使用了mq来实现,比如rabbitmq,rocketmq等等,但是这些mq都是需要安装的,而且还需要配置,对于此项目来说不想增加额外的依赖,所以就想到了使用redis来实现一个简易的延时队列。
二、实现思路
1. 业务场景
订单超时未支付,需要取消订单,这个业务场景可以分为两个步骤来实现:
- 用户下单后,将订单信息存入数据库,并将订单信息存入延时队列中,设置延时时间为30分钟。
- 30分钟后,从延时队列中取出订单信息,判断订单是否已支付,如果未支付,则取消订单。
- 如果用户在30分钟内支付了订单,则将订单从延时队列中删除。
2. 实现思路
- 使用redis的zset来实现延时队列,zset的score用来存储订单的超时时间,value用来存储订单信息。
- 使用redis的set来存储已支付的订单,set中的value为订单id。
三、实现代码
1. 使用了两个注解类分别标记生产者类、生产者方法,消费者方法
/**
* @program:
* @description: redis延时队列生产者类注解,标记生产者类,用来扫描生产者类中的生产者方法,将生产者方法注册到redis延时队列中
* @author: jiangchengxuan
* @created: 2023/12/09 10:32
*/
@Component
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisMessageQueue {}
/**
* @program:
* @description:
* 带有此注解的方法,方法的入参首先会被转换为json字符串,然后存入redis的zset中,score为当前时间+延时时间,value为json字符串
* 当延时时间到达后,会从redis的zset中取出value,然后将value转换为入参类型,调用此方法,执行业务逻辑
* 此注解只能标记在方法上,且方法必须为public,且只能有一个参数
* 此注解标记的方法,必须在redis延时队列生产者类中,否则不会生效
* @author: jiangchengxuan
* @created: 2023/12/09 10:37
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisMessageQueueMethod {
String threadName() default "redis消息队列默认线程";
String queueKey(); // 队列key值
int threadNum() default 1; //默认线程数量
int threadSleepTime() default 500; //默认线程休眠时间默认500ms
}
2. 生产者类具体实现
/**
* @program:
* @description: 生产者类具体实现
* @author: jiangchengxuan
* @created: 2023/12/09 10:44
*/
@Slf4j
@Component
public class DelayQueueWorkerConfig implements InitializingBean {
private volatile boolean monitorStarted = false;
private volatile boolean monitorShutDowned = false;
private ExecutorService executorService;
// 需要监控的延时队列
@Autowired
protected IDelayQueue<String> monitorQueue;
@Autowired
private ApplicationContext applicationContext;
@Override
public void afterPropertiesSet(){
//spring工具类,可以获取指定注解的类
Map<String, Object> allNeedClass = applicationContext.getBeansWithAnnotation(RedisMessageQueue.class);
for (Map.Entry<String, Object> entry : allNeedClass.entrySet()) {
Object bean = entry.getValue();
Method[] methods = bean.getClass().getMethods();
for (Method method : methods) {
Annotation[] annotations = method.getDeclaredAnnotations();
for (Annotation annotation : annotations) {
if (annotation instanceof RedisMessageQueueMethod) {
RedisMessageQueueMethod queueMethod = (RedisMessageQueueMethod) annotation;
//找的需要使用消息队列的方法后,
initExecuteQueue(queueMethod, method, bean);
}
}
}
}
}
/**
* 初始化执行造作
* @param queueAnnotations 注解
* @param method 方法
* @param bean 对象
*/
void initExecuteQueue(RedisMessageQueueMethod queueAnnotations ,Method method,Object bean) {
String threadName = queueAnnotations.threadName();
int threadNum = queueAnnotations.threadNum();
int threadSheepTime = queueAnnotations.threadSleepTime();
String queueKey = queueAnnotations.queueKey();
//获取所有消息队列名称
executorService = Executors.newFixedThreadPool(threadNum);
for (int i = 0; i < threadNum; i++) {
final int num = i;
executorService.execute(() -> {
Thread.currentThread().setName(threadName + "[" + num + "]");
//如果没有设置队列queuekey或者已经暂停则不执行
while (!monitorShutDowned) {
String value = null;
try {
value = monitorQueue.get(queueKey);
// 获取数据时进行删除操作,删除成功,则进行处理,业务逻辑处理失败则继续添加回队列但是时间设置最大以达到保存现场的目的,防止并发获取重复数据
if (StringUtils.isNotEmpty(value)) {
if (log.isDebugEnabled()) {
log.debug("Monitor Thread[" + Thread.currentThread().getName() + "], get from queue,value = {}", value);
}
boolean success = (Boolean) method.invoke(bean, value);
// 失败重试
if (!success) {
success = (Boolean) method.invoke(bean, value);;
if (!success) {
log.warn("Monitor Thread[" + Thread.currentThread().getName() + "] execute Failed,value = {}", value);
monitorQueue.add(TimeUnit.DAYS,365, value, queueKey);
}
} else {
if (log.isDebugEnabled()) {
log.debug("Monitor Thread[" + Thread.currentThread().getName() + "]:execute successfully!values = {}", value);
}
}
} else {
if (log.isDebugEnabled()) {
log.debug("Monitor Thread[" + Thread.currentThread().getName() + "]:monitorThreadRunning = {}", monitorStarted);
}
Thread.sleep(threadSheepTime);
}
} catch (Exception e) {
log.error("Monitor Thread[" + Thread.currentThread().getName() + "] execute Failed,value = " + value, e);
}
}
log.info("Monitor Thread[" + Thread.currentThread().getName() + "] Completed...");
});
}
log.info("thread pool is started...");
}
}
/**
* @program:
* @description:
* 延时队列接口实现类,
* 使用redis的zset实现延时队列,
* @author: jiangchengxuan
* @created: 2023/12/09 23:34
*/
public interface IDelayQueue <E> {
/**
* 向延时队列中添加数据
*
* @param score 分数
* @param data 数据
* @return true 成功 false 失败
*/
boolean add(long score, E data,String queueKey);
/**
* 向延时队列中添加数据
*
* @param timeUnit 时间单位
* @param time 延后时间
* @param data 数据
* @param queueKey
* @return true 成功 false 失败
*/
boolean add(TimeUnit timeUnit, long time, E data, String queueKey);
/**
* 从延时队列中获取数据
* @param queueKey 队列key
* @return 数据
*/
String get(String queueKey);
/**
* 删除数据
*
* @param key
* @param data 数据
* @return
*/
public<T> boolean rem(String key, T data) ;
}
/**
* @program:
* @description: redis操作类,封装了redis的操作方法,使用时直接注入即可使用,不需要关心redis的操作细节,使用时只需要关心业务逻辑即可
* @author: jiangchengxuan
* @created: 2023/12/09 23:35
*/
@Service
public class RedisDelayQueue implements IDelayQueue<String> {
@Autowired
private RedisService redisService;
@Override
public boolean add(long score, String data,String queueKey) {
return redisService.opsForZSet(Constant.DEFAULT_REDIS_QUEUE_KEY_PREFIX+queueKey, data, score);
}
@Override
public boolean add(TimeUnit timeUnit, long time, String data, String queueKey) {
switch (timeUnit) {
case SECONDS:
return add(LocalDateTime.now().plusSeconds(time).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(), data, queueKey);
case MINUTES:
return add(LocalDateTime.now().plusMinutes(time).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(), data,queueKey);
case HOURS:
return add(LocalDateTime.now().plusHours(time).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(), data,queueKey);
case DAYS:
return add(LocalDateTime.now().plusDays(time).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(), data,queueKey);
default:
return false;
}
}
@Override
public String get(String queueKey) {
long now = System.currentTimeMillis();
long min = Long.MIN_VALUE;
Set<String> res = redisService.rangeByScoreZSet(Constant.DEFAULT_REDIS_QUEUE_KEY_PREFIX+queueKey, min, now, 0, 10);
if (!CollectionUtils.isEmpty(res)) {
for (String data : res){
// 删除成功,则进行处理,防止并发获取重复数据
if (rem(queueKey, data)){
return data;
}
}
}
return null;
}
@Override
public<T> boolean rem(String key, T data) {
return redisService.remZSet(Constant.DEFAULT_REDIS_QUEUE_KEY_PREFIX+key, data);
}
}
- 使用
@RedisMessageQueue
public class SomethingClass
{
@Autowired
private IDelayQueue<String> messageQueue;
/**
* 生产者,向队列中添加数据,30秒后消费者进行消费
*/
public void test(){
messageQueue.add(TimeUnit.SECONDS,30L,"这是参数数据","new_queue");
}
/**
* 消费者,如果按此配置的话,会启动一个线程,线程名称为:测试线程名称,线程数量为1,线程休眠时间为10毫秒
* 注意:queueKey需要与生产者中的queueKey保持一致才能进行消费
* @param data
*/
@Override
@RedisMessageQueueMethod(threadName = "测试线程名称",queueKey = "new_queue",threadNum = 1,threadSleepTime = 10)
public void testMethod(String data) {
//do something
}
}
基于Redis的简易延时队列的更多相关文章
- 基于redis的简易分布式爬虫框架
代码地址如下:http://www.demodashi.com/demo/13338.html 开发环境 Python 3.6 Requests Redis 3.2.100 Pycharm(非必需,但 ...
- Delayer 基于 Redis 的延迟消息队列中间件
Delayer 基于 Redis 的延迟消息队列中间件,采用 Golang 开发,支持 PHP.Golang 等多种语言客户端. 参考 有赞延迟队列设计 中的部分设计,优化后实现. 项目链接:http ...
- 灵感来袭,基于Redis的分布式延迟队列(续)
背景 上一篇(灵感来袭,基于Redis的分布式延迟队列)讲述了基于Java DelayQueue和Redis实现了分布式延迟队列,这种方案实现比较简单,应用于延迟小,消息量不大的场景是没问题的,毕竟J ...
- PHP基于Redis实现轻量级延迟队列
延迟队列,顾名思义它是一种带有延迟功能的消息队列. 那么,是在什么场景下我才需要这样的队列呢? 一.背景 先看看一下业务场景: 1.会员过期前3天发送召回通知 2.订单支付成功后,5分钟后检测下游环节 ...
- 灵感来袭,基于Redis的分布式延迟队列
延迟队列 延迟队列,也就是一定时间之后将消息体放入队列,然后消费者才能正常消费.比如1分钟之后发送短信,发送邮件,检测数据状态等. Redisson Delayed Queue 如果你项目中使用了re ...
- [转载] 基于Redis实现分布式消息队列
转载自http://www.linuxidc.com/Linux/2015-05/117661.htm 1.为什么需要消息队列?当系统中出现“生产“和“消费“的速度或稳定性等因素不一致的时候,就需要消 ...
- redis实现简单延时队列(转)
继之前用rabbitMQ实现延时队列,Redis由于其自身的Zset数据结构,也同样可以实现延时的操作 Zset本质就是Set结构上加了个排序的功能,除了添加数据value之外,还提供另一属性scor ...
- laravel5.6 基于redis,使用消息队列(邮件推送)
邮件发送如何配置参考:https://www.cnblogs.com/clubs/p/10640682.html 用到的用户表: CREATE TABLE `recruit_users` ( `id` ...
- 基于redis的延迟消息队列设计
需求背景 用户下订单成功之后隔20分钟给用户发送上门服务通知短信 订单完成一个小时之后通知用户对上门服务进行评价 业务执行失败之后隔10分钟重试一次 类似的场景比较多 简单的处理方式就是使用定时任务 ...
- 基于redis的延迟消息队列设计(转)
需求背景 用户下订单成功之后隔20分钟给用户发送上门服务通知短信 订单完成一个小时之后通知用户对上门服务进行评价 业务执行失败之后隔10分钟重试一次 类似的场景比较多 简单的处理方式就是使用定时任务 ...
随机推荐
- Linux 网络发包流程
哈喽大家好,我是咸鱼 之前咸鱼在<Linux 网络收包流程>一文中介绍了 Linux 是如何实现网络接收数据包的 简单回顾一下: 数据到达网卡之后,网卡通过 DMA 将数据放到内存分配好的 ...
- 【路由器】小米 WR30U 解锁并刷机
本文主要记录个人对小米 WR30U 路由器的解锁和刷机过程,整体步骤与 一般安装流程 类似,但是由于 WR30U 的解锁 ssh 和刷机的过程中有一些细节需要注意,因此记录一下 解锁 ssh 环境准备 ...
- langchain中的LLM模型使用介绍
简介 构建在大语言模型基础上的应用通常有两种,第一种叫做text completion,也就是一问一答的模式,输入是text,输出也是text.这种模型下应用并不会记忆之前的问题内容,每一个问题都是最 ...
- Redis的五大数据类型的数据结构
概述 Redis底层有六种数据类型包括:简单动态字符串.双向链表.压缩列表.哈希表.跳表和整数数组.这六种数据结构五大数据类型关系如下: String:简单动态字符串 List:双向链表.压缩列表 ...
- Laf 云开发平台及其实现原理
Laf 产品介绍 自我介绍 大家好,我是来自 Laf 团队的王子俊,很高兴今天能在这里给大家分享我们 Laf 云开发平台及其实现原理.本来想说一点什么天气之类的话作为开头,但主持人都说完啦,我就不多说 ...
- Spring Boot虚拟线程与Webflux在JWT验证和MySQL查询上的性能比较
早上看到一篇关于Spring Boot虚拟线程和Webflux性能对比的文章,觉得还不错.内容较长,我就不翻译了,抓重点给大家介绍一下这篇文章的核心内容,方便大家快速阅读. 测试场景 作者采用了一个尽 ...
- Mybatis中的设计模式
最近在看<通用源码阅读指导书:Mybatis源码详解>,这本书一一介绍了Mybatis中的各个包的功能,同时也涉及讲了一些阅读源码的技巧,还讲了一些源码中涉及的设计模式,这是本篇文章介绍的 ...
- 若依(ruoyi)开源系统保姆级实践-完成第一个页面
一.案例描述 若依官网文档地址:http://doc.ruoyi.vip/ruoyi/document/hjbs.html 本教程主要内容,自定义数据库表,使用若依开源系统生成代码并配置权限. 若依环 ...
- 《流畅的Python》 读书笔记 第二章数据结构(2) 231011
2.5 对序列使用+和* 通常 + 号两侧的序列由相同类型的数据所构成,在拼接的过程中,两个被操作的序列都不会被修改,Python 会新建一个包含同样类型数据的序列来作为拼接的结果 +和*都遵循这个规 ...
- D 算法模板(Boruvka's Algorithm)
Description 为了方便你测试,本题为D题简化版. You are given a complete undirected graph with nn vertices. A number a ...