基于Redis的简易延时队列

一、背景

在实际的业务场景中,经常会遇到需要延时处理的业务,比如订单超时未支付,需要取消订单,或者是用户注册后,需要在一段时间内激活账号,否则账号失效等等。这些业务场景都可以通过延时队列来实现。

最近在实际业务当中就遇到了这样的一个场景,需要实现一个延时队列,用来处理订单超时未支付的业务。在网上找了一些资料,发现大部分都是使用了mq来实现,比如rabbitmq,rocketmq等等,但是这些mq都是需要安装的,而且还需要配置,对于此项目来说不想增加额外的依赖,所以就想到了使用redis来实现一个简易的延时队列。

二、实现思路

1. 业务场景

订单超时未支付,需要取消订单,这个业务场景可以分为两个步骤来实现:

  1. 用户下单后,将订单信息存入数据库,并将订单信息存入延时队列中,设置延时时间为30分钟。
  2. 30分钟后,从延时队列中取出订单信息,判断订单是否已支付,如果未支付,则取消订单。
  3. 如果用户在30分钟内支付了订单,则将订单从延时队列中删除。

2. 实现思路

  1. 使用redis的zset来实现延时队列,zset的score用来存储订单的超时时间,value用来存储订单信息。
  2. 使用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);
}
}
  1. 使用
@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的简易延时队列的更多相关文章

  1. 基于redis的简易分布式爬虫框架

    代码地址如下:http://www.demodashi.com/demo/13338.html 开发环境 Python 3.6 Requests Redis 3.2.100 Pycharm(非必需,但 ...

  2. Delayer 基于 Redis 的延迟消息队列中间件

    Delayer 基于 Redis 的延迟消息队列中间件,采用 Golang 开发,支持 PHP.Golang 等多种语言客户端. 参考 有赞延迟队列设计 中的部分设计,优化后实现. 项目链接:http ...

  3. 灵感来袭,基于Redis的分布式延迟队列(续)

    背景 上一篇(灵感来袭,基于Redis的分布式延迟队列)讲述了基于Java DelayQueue和Redis实现了分布式延迟队列,这种方案实现比较简单,应用于延迟小,消息量不大的场景是没问题的,毕竟J ...

  4. PHP基于Redis实现轻量级延迟队列

    延迟队列,顾名思义它是一种带有延迟功能的消息队列. 那么,是在什么场景下我才需要这样的队列呢? 一.背景 先看看一下业务场景: 1.会员过期前3天发送召回通知 2.订单支付成功后,5分钟后检测下游环节 ...

  5. 灵感来袭,基于Redis的分布式延迟队列

    延迟队列 延迟队列,也就是一定时间之后将消息体放入队列,然后消费者才能正常消费.比如1分钟之后发送短信,发送邮件,检测数据状态等. Redisson Delayed Queue 如果你项目中使用了re ...

  6. [转载] 基于Redis实现分布式消息队列

    转载自http://www.linuxidc.com/Linux/2015-05/117661.htm 1.为什么需要消息队列?当系统中出现“生产“和“消费“的速度或稳定性等因素不一致的时候,就需要消 ...

  7. redis实现简单延时队列(转)

    继之前用rabbitMQ实现延时队列,Redis由于其自身的Zset数据结构,也同样可以实现延时的操作 Zset本质就是Set结构上加了个排序的功能,除了添加数据value之外,还提供另一属性scor ...

  8. laravel5.6 基于redis,使用消息队列(邮件推送)

    邮件发送如何配置参考:https://www.cnblogs.com/clubs/p/10640682.html 用到的用户表: CREATE TABLE `recruit_users` ( `id` ...

  9. 基于redis的延迟消息队列设计

    需求背景 用户下订单成功之后隔20分钟给用户发送上门服务通知短信 订单完成一个小时之后通知用户对上门服务进行评价 业务执行失败之后隔10分钟重试一次 类似的场景比较多 简单的处理方式就是使用定时任务 ...

  10. 基于redis的延迟消息队列设计(转)

    需求背景 用户下订单成功之后隔20分钟给用户发送上门服务通知短信 订单完成一个小时之后通知用户对上门服务进行评价 业务执行失败之后隔10分钟重试一次 类似的场景比较多 简单的处理方式就是使用定时任务 ...

随机推荐

  1. SRC赏金猎人—笔记一

    以下是我如何将 webshell 上传到一个旧目标中, 这是使用谷歌dorks,Js检查和文件上传过滤器绕过. 过程 1.我随机选择了一个范围很大的目标开始 2.我启动了自动化脚本来发现使用的技术.d ...

  2. 知识图谱(Knowledge Graph)- Neo4j 5.10.0 使用 - Python 操作

    数据基于: 知识图谱(Knowledge Graph)- Neo4j 5.10.0 使用 - CQL - 太极拳传承谱系表 这是一个非常简单的web应用程序,它使用我们的Movie图形数据集来提供列表 ...

  3. 程序员:你如何写可重复执行的SQL语句?

    上图的意思: 百战百胜,屡试不爽. 故事 程序员小张: 刚毕业,参加工作1年左右,日常工作是CRUD 架构师老李: 多个大型项目经验,精通各种开发架构屠龙宝术: 小张注意到,在实际的项目开发场景中,很 ...

  4. 快速理解DDD领域驱动设计架构思想-基础篇

    1 前言 本文与大家一起学习并介绍领域驱动设计(Domain Drive Design) 简称DDD,以及为什么我们需要领域驱动设计,它有哪些优缺点,尽量用一些通俗易懂文字来描述讲解领域驱动设计,本篇 ...

  5. Kali-Linux-配置开发环境

    本文主要讲解JDK.SDK.eclipse-adt.android studio.cpu模式TensorFlow 的安装配置.update:2019-08-30 03:31:46 JDK 当前系统jd ...

  6. 「Tricks」整体DP

    不太了解这个东西的具体定义是什么,总之应该是一个用数据结构维护 DP 状态的某几个维度的 trick 吧. 事实上你可以把这篇 post 理解为三个题的解集. 先直接来看 noi2020 - Dest ...

  7. E-GraphSAGE: A Graph Neural Network based Intrusion Detection System 笔记

    E-GraphSAGE: A Graph Neural Network based Intrusion Detection System 目录 E-GraphSAGE: A Graph Neural ...

  8. oracle:ORA-14765建索引阻塞创建分区及处理步骤

    在生产库建立一个索引,报ORA-14765创建索引时不能创建分区,也就是索引的创建阻塞分区的建立. 处理步骤: 1.与开发人员沟通昨天下午在Tbl_Waste表上建索引,一直未返回成功,定位问题SQL ...

  9. webgl centroid质心插值的一点理解

    质心插值说的是什么 2023.10.04再次review这个细节点: https://www.opengl.org/pipeline/article/vol003_6/ https://github. ...

  10. 研发提速:nacos+openfeign环境下的本地链接服务

    项目研发过程中,经常会遇到与测试人员工作重叠的情况,十分影响效率. 做了一个修改,可以在本地环境启动项目后和测试环境交互,并且不影响测试环境,理论上也可以用于线上环境的异常的快速处理. 准备事项如下: ...