基于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分钟重试一次 类似的场景比较多 简单的处理方式就是使用定时任务 ...
随机推荐
- *CTF和nssctf#16的wp
*ctf2023 fcalc 分析程序 本题存在漏洞,是生活中很容易犯的错误,就是循环或者判断的时候没有注意多一还是少一,这种会发生很严重的问题.比如这个题在过滤数字的时候没有过滤掉0,所以输入0的时 ...
- 6、Spring之基于xml的自动装配
6.1.场景模拟 6.1.1.创建UserDao接口及实现类 package org.rain.spring.dao; /** * @author liaojy * @date 2023/8/5 - ...
- 三维模型OSGB格式轻量化压缩必要性分析
三维模型OSGB格式轻量化压缩必要性分析 三维模型是计算机图形学和视觉效果等领域的重要应用之一.然而,由于三维模型通常包含大量的几何信息.纹理信息和其他元素,导致其占用的存储空间和计算资源非常巨大.为 ...
- Redis系列20:LFU内存淘汰算法分析
Redis系列1:深刻理解高性能Redis的本质 Redis系列2:数据持久化提高可用性 Redis系列3:高可用之主从架构 Redis系列4:高可用之Sentinel(哨兵模式) Redis系列5: ...
- 在Windows下用VScode构造shell脚本的IDE
在linux系统中,大家可以很轻松的开发.调试shell脚本.但是,对于不熟悉linux系统 的小白或者想在Windows下开发shell脚本的人来说,这就有点不友好了.本篇文章就 教大家,在Wind ...
- 《CTFshow-Web入门》05. Web 41~50
@ 目录 web41 题解 原理 web42 题解 原理 web43 题解 原理 web44 题解 原理 web45 题解 原理 web46 题解 原理 web47 题解 web48 题解 web49 ...
- 2022最新 Navicat Premium 16中文软件激活安装永久使用正版(支持MAC+win)
Navicat Premium 16中文正版永久使用,下载地址: 关注我的wx公众号"奋斗在IT"回复1015获取下载地址
- 入门篇-其之一-第一个Java程序
️注意: 本文中包含实际操作,需要安装JDK.如果需要安装JDK,请按照这篇文章的步骤进行安装:点我查看JDK安装教程 小白可以多看几遍这篇文章,多敲几次代码 前面我们已经安装了JDK,接下来就是写一 ...
- Fabric 2.x 智能合约开发记录
表象:Return schema invalid. required items must be unique [recovered] 虽然 Fabric v2.2 已经发布了很久了,但之前因为项目历 ...
- CF1294C
题目简化和分析: 使得 \(a \times b\times c=n~~~~(a\ne b\ne c)\) 思路: 先枚举最小的 \(a\) ,(以下程序的返回值为最小因数(有部分优化),即 \(a\ ...