基于Redis的延时任务队列
- 一.背景
- 二.整体构架
- 设计思路
- 构架图如下:
- 三.代码类图
- DelayJob(任务详情)
- WaitQueue(延时队列)
- ReadyQueue(就绪队列)
- Scanner(扫描线程,轮询任务)
- 四.使用
- Maven依赖
- spring mvc中使用
- spring boot中使用
- 五.源码地址
一.背景
笔者先前遇到了一个订单超时关闭的问题,首先就排除了:起一任务轮询数据库的方案,太耗资源,也增加DB的负担。查阅了一些资料,RocketMQ可以实现延时消息的功能,下单的时候推一个延时24小时的MQ消息,24小时后系统再次收到这个消息时检测订单是否是待付款状态,如果是,则关闭订单。由于笔者的后台服务使用的是阿里云ONS(基于RocketMQ),于是就这样完成了需求。
但是,如果不幸的是你的服务没有使用RocketMQ,如ActiveMQ等不支持延时消息的消息中间件怎么办?可以基于Redis的SkipList(跳跃表), Redis Client的命令是zset(sorted set)、zadd 等。简而言之,就是在set key的时候,除了value还可以添加一个score, 当你取出key中取出value时可以指定取出指定范围score,本篇博文就是基于Redis跳跃表实现的。
二.整体构架
设计思路
- 用redis zset存放延时任务队列,这个队列只放Delay Queue任务id
- 用一个队列queue存放就绪任务队列,这个队列也只放任务id
- 任务的详细信息通过一个redis map结构存储,以任务id为key, 任务详情为value
- 任何详情包含任务的id、任务topic、执行时刻(score)、失败重试次数、失败后重试间隔、任务内容等
- 一个线程(也可以多个)从Delay Queue轮询当前时刻需要执行的任务,放置到Ready Queue
- 另外一个组线程轮询Ready Queue,交给指定Topic的Job Handler去执行任务,当任务执行成功或达到失败重试次数后,从Job Detail Pool中删除对应id的任务。如果任务失败,则重新放到Delay Queue,消耗一次重试次数
构架图如下:
三.代码类图
DelayJob(任务详情)
public class DelayJob<T> {
/**
* job id
*/
private String id;
/**
* 消息类型
*/
private String topic;
/**
* 任务执行时间(时间戳:精确到秒)
*/
private Long execTime;
/**
* 重试次数
*/
private Integer retryTimes = 0;
/**
* 消费失败,重新消费间隔(单位秒)
* 默认0L, 消费失败不重新消费
*/
private Long retryDelay = 0L;
/**
* 消息体
*/
private T body;
}
WaitQueue(延时队列)
ReadyQueue(就绪队列)
Scanner(扫描线程,轮询任务)
四.使用
Maven依赖
<dependency>
<groupId>com.github.rxyor</groupId>
<artifactId>carp-distributed</artifactId>
<version>1.0.4</version>
</dependency>
spring mvc中使用
xml配置bean示例:
<bean id="fastJsonCodec" class="com.github.rxyor.redis.redisson.codec.FastJsonCodec"/>
<bean id="delayRedisConfig"
class="com.github.rxyor.redis.redisson.config.RedisConfig">
<property name="host" value="${redis.host}"/>
<property name="port" value="${redis.port}"/>
<property name="password" value="${redis.password}"/>
<property name="database" value="${redis.database}"/>
<property name="codec" ref="fastJsonCodec"/>
</bean>
<bean id="redisDelayJobConfig"
class="com.github.rxyor.distributed.redisson.delay.config.DelayConfig">
<property name="appName" value="${redis.app}"/>
</bean>
<bean id="scanWrapper" class="com.github.rxyor.distributed.redisson.delay.core.ScanWrapper"
init-method="initAndScan" destroy-method="destroy">
<property name="delayConfig" ref="redisDelayJobConfig"/>
<property name="redisConfig" ref="delayRedisConfig"/>
<property name="handlerList">
<list>
<bean class="com.github.rxyor.distributed.redisson.delay.handler.LogJobHandler">
<property name="topic" value="girl"/>
</bean>
</list>
</property>
</bean>
<bean id="delayClientProxy" factory-bean="scanWrapper" factory-method="getDelayClientProxy"/>
定义一个controller用于测试:
@Api(value = "延时队列")
@RestController
@AllArgsConstructor
@RequestMapping("/delay")
public class DelayJobController {
private final DelayClientProxy delayClientProxy;
@ApiOperation(value = "添加一个延时任务", httpMethod = "POST")
@PostMapping("/job/add")
@ResponseBody
public R addDelayJob(@RequestBody DelayJob<Map<String, Object>> delayJob) {
log.info("received a job ,current time is:{}", System.currentTimeMillis() / 1000);
//没有执行时间,设置为当前时间延时10秒后执行
if (delayJob.getExecTime() == null) {
delayJob.setExecTime(System.currentTimeMillis() / 1000 + 10);
}
delayClientProxy.offer(delayJob);
return RUtil.success(delayJob);
}
}
启动我们的spring项目,发送一个请求,请求参数如下:
{
"body": {"girl":"I Like You"},
"retryDelay": 10,
"retryTimes": 3,
"topic": "girl"
}
由于我们一个LogJobHandler,大概10秒后会打印Job的内容,如图所示:
这里两条打印是因为,LogJobHandler 执行了一次System.out.println和Log, LogJobHandler只是一个示例的Handler,你可以自定义JobHandler进行业务操作。
spring boot中使用
配置bean:
@Configuration
public class DelayQueueConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private Integer port;
@Value("${spring.redis.database}")
private Integer database;
@Value("${spring.redis.password}:''")
private String redisPassword;
@Bean
public RedisConfig redisConfig() {
RedisConfig redisConfig = new RedisConfig();
redisConfig.setHost(redisHost);
redisConfig.setPort(port);
redisConfig.setPassword(redisPassword);
redisConfig.setDatabase(database);
return redisConfig;
}
@Bean
public com.github.rxyor.distributed.redisson.delay.config.DelayConfig delayConfig() {
com.github.rxyor.distributed.redisson.delay.config.DelayConfig delayConfig = new com.github.rxyor.distributed.redisson.delay.config.DelayConfig();
delayConfig.setAppName("carp-boot");
return delayConfig;
}
@Bean
public List<JobHandler> handlerList() {
List<JobHandler> handlerList = new ArrayList<>(4);
LogJobHandler logJobHandler = new LogJobHandler();
logJobHandler.setTopic("girl");
handlerList.add(logJobHandler);
return handlerList;
}
@Bean(initMethod = "initAndScan", destroyMethod = "destroy")
public ScanWrapper scanWrapper() {
ScanWrapper scanWrapper = new ScanWrapper();
scanWrapper.setRedisConfig(redisConfig());
scanWrapper.setDelayConfig(delayConfig());
scanWrapper.setHandlerList(handlerList());
return scanWrapper;
}
@Bean
public DelayClientProxy delayClientProxy() {
return scanWrapper().getDelayClientProxy();
}
}