基于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分钟重试一次 类似的场景比较多 简单的处理方式就是使用定时任务 ...
随机推荐
- [Lua][Love Engine] 打砖块游戏实现过程与知识点
本文旨在根据LOVE2D官方文档和教程实现打砖块的游戏,记录部分实现过程和重要知识点 目标摧毁所有砖块 玩家控制球拍左右滑动反弹小球 小球摧毁砖块 小球保持在屏幕内 小球碰到屏幕底部,GAME OVE ...
- typora使用教程&高级用法&Markdown
typora使用教程&高级用法&Markdown typora介绍 哇啦哇啦哇啦哇,,,,,,,,,,,,, 提示:小白看不懂的话,建议哔哩哔哩搜索"遇见狂神说", ...
- TCP 粘包
TCP(Transmission Control Protocol,传输控制协议)是一种传输层协议. TCP提供了以下主要功能: 可靠性:TCP使用确认.重传和校验等机制来确保数据的可靠传输.它能够检 ...
- Java NIO 图解 Netty 服务端启动的过程
一.启动概述 了解整体Netty常用的核心组件后,并且对比了传统IO模式.在对比过程中,找到了传统IO对应Netty中是如何实现的.最后我们了解到在netty中常用的那些组件. 本文在了解下这些核心组 ...
- shopee商品详情接口的应用
Shopee是东南亚和台湾地区最大的电子商务平台之一,成立于2015年,目前覆盖6个国家和地区.作为一家新兴电商平台,Shopee拥有快速增长的销售额和庞大的用户群体,为开发者提供了丰富的商业机会.其 ...
- 一个.NET 7 + DDD + CQRS +React+Vite的实战项目
项目简介 基于SignalR实现聊天通信,支持横向扩展,可支撑上万用户同时在线聊天 后端架构 后端技术栈采用 .NET 7 PostgreSQL (业务数据库) Redis(用于存放热点数据,和支持S ...
- Java下载多个网络文件并打成压缩包
需求:浏览器访问后台的http地址后,后台将多个网络文件打成压缩包返回给浏览器,用户可以通过浏览器直接下载压缩包. 实现: 根据文件链接把文件下载下来并且转成字节码 ,代码: package com ...
- ubuntu22.04.3 安装postgresql 16 rc1数据库
ubuntu22.04.3 安装postgresql 16 rc1数据库 一.直接安装 # Create the file repository configuration: sudo sh -c ' ...
- MyBatis-Plus和PageHelper冲突导致Factory method sqlSessionFactory threw exception,并且如何分页显示全部
springboot开始引入了mybaits-plus.后来想引入pagehelper进行分页,引入之后报错 Error starting ApplicationContext. To display ...
- 从DevOps实践落地的角度谈谈“流程”和“规范"的反模式
最近在经历的一些事情,让我突发灵感,觉得要写点关于DevOps体系建设过程中的"流程规范",记录下来. 如何解读"流程规范" 谈到DevOps落地,无一例外都会 ...