理论介绍
首先贴图来说明redis是如何实现延时队列的
当用户发送一个消息请求给服务器后台的时候,服务器会检测这条消息是否需要进行延时处理,如果需要就放入到延时队列中,由延时任务检测器进行检测和处理,对于不需要进行延时处理的任务,服务器会立马对消息进行处理,并把处理后的结果返会给用户。
对于在延时任务检测器内部的话,有查询延迟任务和执行延时任务两个职能,任务检测器会先去延时任务队列进行队列中信息读取,判断当前队列中哪些任务已经时间到期并将已经到期的任务输出执行(设置一个定时任务)。
这时,我们可以想一想在Redis的数据结构中有哪些能进行时间设置标志的命令?
是不是想到的 zset 这个命令,具有去重有序(分数排序)的功能。没错,你想对了呀!
我们可以使用 zset(sortedset)这个命令,用设置好的时间戳作为score进行排序,使用 zadd score1 value1 …命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务,通过循环执行队列任务即可。也可以通过 zrangebyscore key min max withscores limit 0 1 查询最早的一条任务,来进行消费。
总的来说,你可以通过以下两种方式来实现:
(1)使用zrangebyscore来查询当前延时队列中所有任务,找出所有需要进行处理的延时任务,在依次进行操作。
(2)查找当前最早的一条任务,通过score值来判断任务执行的时候是否大于了当前系统的时候,比如说:最早的任务执行时间在3点,系统时间在2点58分),表示这个应该需要立马被执行啦,时间快到了。
代码实现
生产消息的RedisController
package testredis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
/**
*
* @author nycp42j guobushi
* @version : RedisController.java, v 0.1 2021-12-25 4:56 PM guobushi Exp $$
*/
@RestController
@RequestMapping("/redis")
public class RedisController {
@Autowired
private RedisUtil redisUtil;
private static String QUEUE_NAME = "redis_delay_queue";
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 模仿生产者
@RequestMapping("/product")
public String product() {
for (int i = 1; i <= 1; i++) {
Calendar now = Calendar.getInstance();
now.setTime(new Date());
// 获取当前秒数 并把消费时间设为当前时间的20秒以后
now.set(Calendar.SECOND, now.get(Calendar.SECOND) + 20);
System.out.println("生产了:" + i + "当前时间为:" + simpleDateFormat.format(System.currentTimeMillis()) + "消费时间为:" + simpleDateFormat.format(now.getTime()));
// 往QUEUE_NAME这个集合中放入i,设置score为排序规则
redisUtil.opsForZsetAdd(QUEUE_NAME, i, now.getTime().getTime());
}
return "生产者生产消息成功";
}
}
自动执行的消费消息的定时任务
package testredis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import java.text.SimpleDateFormat;
import java.util.Iterator;
import java.util.Set;
/**
*
* @author nycp42j guobushi
* @version : ConsumeConfiguration.java, v 0.1 2021-12-26 1:16 PM guobushi Exp $$
*/
// 项目启动就开始执行的定时任务
//@Configuration
//@EnableScheduling
public class ConsumeConfiguration {
@Autowired
private RedisUtil redisUtil;
private static String QUEUE_NAME = "redis_delay_queue";
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 模仿消费者 每秒执行一次
@Scheduled(cron = "*/1 * * * * * ")
public void consume(){
System.out.println("------------等待消费--------------");
// 取出QUEUE_NAME集合中的score在0-当前时间戳这个范围的所有值
Set<Object> set = redisUtil.opsForZSetrangeByScore(QUEUE_NAME, 0, System.currentTimeMillis());
Iterator<Object> iterator = set.iterator();
while (iterator.hasNext()) {
Integer value = (Integer) iterator.next();
// 遍历取出每一个score
Double score = redisUtil.opsForZSetScore(QUEUE_NAME, value);
// 达到了时间就进行消费
if (System.currentTimeMillis() > score) {
System.out.println("消费了:" + value + "消费时间为:" + simpleDateFormat.format(System.currentTimeMillis()));
redisUtil.opsForZSetRemove(QUEUE_NAME, value);
}
}
}
}
如果不想用springboot的注解自动执行定时任务,还可以采用手动的方式:
package testredis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
import java.util.concurrent.ScheduledFuture;
/**
*
* @author nycp42j guobushi
* @version : Task.java, v 0.1 2021-12-26 2:31 PM guobushi Exp $$
*/
@RestController
@RequestMapping("/task")
public class TaskScheduleController {
@Autowired
private ThreadPoolTaskScheduler threadPoolTaskScheduler;
@Autowired
private MyRunnable myRunnable;
private ScheduledFuture<?> future1;
@Bean
public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
return new ThreadPoolTaskScheduler();
}
// 手动执行定时任务
@RequestMapping("/startconsume")
public String startCron1() {
future1 = threadPoolTaskScheduler.schedule(myRunnable, new Trigger(){
@Override
public Date nextExecutionTime(TriggerContext triggerContext){
return new CronTrigger("0/1 * * * * *").nextExecutionTime(triggerContext);
}
});
return "定时任务启动成功!";
}
@RequestMapping("/stopconsume")
public String stopCron1() {
if (future1 != null) {
future1.cancel(true);
}
return "定时任务关闭成功!";
}
}
手动执行定时任务的自定义Runnable接口
package testredis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Iterator;
import java.util.Set;
/**
* @author nycp42j guobushi
* @version : MyRunnable.java, v 0.1 2021-12-26 2:33 PM guobushi Exp $$
*/
// @Component 下 @autowired才能生效
@Component
public class MyRunnable implements Runnable {
@Autowired
private RedisUtil redisUtil;
private static String QUEUE_NAME = "redis_delay_queue";
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public void run() {
// 模仿消费者 每秒执行一次
// 取出QUEUE_NAME集合中的score在0-当前时间戳这个范围的所有值
Set<Object> set = redisUtil.opsForZSetrangeByScore(QUEUE_NAME, 0, System.currentTimeMillis());
Iterator<Object> iterator = set.iterator();
while (iterator.hasNext()) {
Integer value = (Integer) iterator.next();
// 遍历取出每一个score
Double score = redisUtil.opsForZSetScore(QUEUE_NAME, value);
// 达到了时间就进行消费
if (System.currentTimeMillis() > score) {
System.out.println("消费了:" + value + "消费时间为:" + simpleDateFormat.format(System.currentTimeMillis()));
redisUtil.opsForZSetRemove(QUEUE_NAME, value);
}
}
}
}
附自定义的RedisUtil工具:
package testredis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
*
* @author nycp42j guobushi
* @version : RedisTemplate.java, v 0.1 2021-12-25 3:02 PM guobushi Exp $$
*/
@Component
public class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
//将 {value} 添加到 {key} 处的排序集合,或者如果它已经存在则更新其 {score}
public <V>void opsForZsetAdd(String key, V value, double score){
redisTemplate.opsForZSet().add(key, value, score);
}
public Set<Object> opsForZSetrangeByScore(String key, double min, double max){
return redisTemplate.opsForZSet().rangeByScore(key, min, max);
}
// 获取zset中指定key的score
public Double opsForZSetScore(String key, Object score) {
return redisTemplate.opsForZSet().score(key, score);
}
// zset删除元素
public void opsForZSetRemove(String key, Object value) {
redisTemplate.opsForZSet().remove(key, value);
}
}