目录
概要
一、实现原理
适用场景
二、准备工作
三、代码实现
四、zset的优缺点
优点
缺点
概要
本文章主要记录的是使用Redis中的zset实现延时任务,在工作中,像这样的的延时任务是不可避免的,举个栗子:买一张火车票,必须在30分钟之内付款,否则该订单被自动取消。订单30分钟不付款自动取消,这个任务就是一个延时任务。
一、实现原理
首先来介绍一下实现原理,我们需要使用redis zset来实现延时任务的需求,所以我们需要知道zset的应用特性。zset作为redis的有序集合数据结构存在,排序的依据就是score。
所以我们可以利用zset score这个排序的这个特性,来实现延时任务
适用场景
- 在用户下单的时候,同时生成延时任务放入redis,key是可以自定义的,比如:delaytask:order
- value的值分成两个部分:
- 一个部分是score用于排序;
- 一个部分是member:member的值我们设置为订单对象(如:订单编号),因为后续延时任务时效达成的时候,我们需要有一些必要的订单信息(如:订单编号),才能完成订单自动取消关闭的动作。
- 延时任务实现的重点:score设置为:订单生成时间+延时时长。这样redis会对zset按照score延时时间进行排序。
- 开启redis扫描任务,获取“当前时间 > score”的延时任务并执行。即:当前时间 > 订单生成时间 + 延时时长 的时候,执行延时任务。
二、准备工作
使用redis zset这个方案来完成延时任务的需求,首先肯定是需要redis,这一点毫无疑问,redis 的环境搭建在这里就不赘述了。其次,使用SpringBoot框架来完成。
Spingboot集成redis:
<!-- 工具类jar包 -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>${commons.collections.version}</version>
</dependency>
<!--redis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Springboot中application.yml配置文件中配置redis相关信息:
spring:
redis:
database: 0 # Redis 数据库索引(默认为 0)
host: 192.168.161.3 # Redis 服务器地址
port: 6379 # Redis 服务器连接端口
password: 123456 # Redis 服务器连接密码(默认为空)
timeout: 5000 # 连接超时,单位ms
lettuce:
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制) 默认 8
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
max-idle: 8 # 连接池中的最大空闲连接 默认 8
min-idle: 0 # 连接池中的最小空闲连接 默认 0
三、代码实现
下面的这个类就是延时任务的核心实现了,一共包含三个核心方法:
- produce方法,用于生成订单——order为订单信息,可以是订单流水号,也可以是订单对象,具体看业务需要,用于延时任务达到时效后关闭订单
- afterPropertiesSet方法是InitialzingBean接口的方法,之所以实现这个接口,是因为我们需要在应用启动的时候开启redis扫描任务。(也可以使用定时任务)即:当OrderDelayServiceImpl bean初始化的时候,开启redis扫描任务循环获取延时任务数据。
- consuming函数,用于从redis获取延时任务数据,消费延时任务,执行超时订单关闭等操作。为了避免阻塞for循环,影响后面延时任务的执行,所以这个consuming函数一定要做成异步的(参考Spring Boot异步任务及Async注解的使用方法)。
package com.techstone.techstone.modules.service.impl;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Set;
/**
* ZSet延时任务
*/
@Service
public class OrderDelayServiceImpl implements InitializingBean {
/**
* redis zset key
*/
public static final String ORDER_DELAY_TASK_KEY = "delaytask:order";
/**
* @ --> @Resource和@Autowired区别
* 1.来源不同:
* 来自不同的父类,@Autowired是Spring定义的注解,@Resource是java定义的注解
* 2.依赖查找的顺序不同:+
* 有两种方式:按名称(byName)查询、按照类型(byType)查找。
* 。@Autowired是先根据类型查找,如果存在多个Bean再根据名称查找;
* 。@Resource是先根据名称查找,如果查不到,再根据类型进行查找。
* 3.支持的参数不同:
* 。@Autowired只支持设置一个required的参数
* 。@Resource支持7个参数,
* 4.依赖注入的用法不同:
* 。@Autowired支持属性注入、构造方法注入、Setter注入
* 。@Resource只支持属性注入和Setter注入
* 5.编译器提示不同:
* 在IDEA中注入的是Mapper对象,使用@Autowired编译器回提示报错信息;
* 使用@Resource就不会了
*/
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 生成订单-order为订单信息,可以是订单流水号,用于延时任务达到时效后关闭订单
* @param orderSerialNo
*/
public void produce(String orderSerialNo){
stringRedisTemplate.opsForZSet().add(
ORDER_DELAY_TASK_KEY, // redis key
orderSerialNo, // zset memeber
System.currentTimeMillis() + (60 * 1000) // zset score
);
}
/**
* 延时任务,也是异步任务,延时任务达到时效之后关闭订单,并将延时任务从redis zset删除
*/
@Async("test")
public void consuming(){
// rangeByScoreWithScores方法用于从redis中获取延时任务,score大于0小于当前时间的所有延时任务
Set<ZSetOperations.TypedTuple<String>> orderSerialNos = stringRedisTemplate.opsForZSet().rangeByScoreWithScores(
ORDER_DELAY_TASK_KEY,
0, // 延时任务score最小值
System.currentTimeMillis() // 延时任务score最大值(当前时间)
);
if(!CollectionUtils.isEmpty(orderSerialNos)){
for (ZSetOperations.TypedTuple<String> orderSerialNo : orderSerialNos) {
// 这里根据orderSerialNo去检查用户是否完成了订单支付
// 如果用户没有支付订单,去执行订单关闭的操作
System.out.println("订单"+ orderSerialNo.getValue() + "超时被自动关闭");
// 订单关闭之后,将订单延时任务从队列中删除
stringRedisTemplate.opsForZSet().remove(ORDER_DELAY_TASK_KEY, orderSerialNo.getValue());
}
}
}
/**
* 该类对象Bean实例化之后,就开启while扫描任务
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
// 开启新的线程,否则SpringBoot应用初始化无法启动
new Thread(()->{
while (true){
try {
// 每5秒扫描一次redis库获取延时数据,不用太频繁没必要
Thread.sleep(5 * 1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
consuming();
}
}).start();
}
}
需要关注的点:
- 上文中的rangeByScoreWithScores方法用于从redis中获取延时任务,score大于0小于当前时间的所有延时任务,都将被从redis里面取出来。每5秒执行一次,所以延时任务的误差不会超过5秒。
- 上文中的订单信息,只保留了订单唯一流水号,用于关闭订单。如果业务需要传递更多的订单信息,请使用RedisTemplate操作订单类对象,而不是StringRedisTemplate操作订单流水号字符串。
模拟订单下单,使用如下操作,将订单序列号放入redis zset中即可实现延时任务,笔者是采用接口的形式测试的。
@GetMapping("/testZSetScore")
@ApiOperation(value = "测试缓存ZSet延时任务")
public Result<String> testZSetScore(@RequestParam("id") String id){
// 主要调用这个方法
orderDelayService.produce(id);
return new Result<String>().ok("已成功将key设置到Redis缓存中");
}
四、zset的优缺点
优点
- 延时任务保存再redis里,redis具有数据持久化的机制,可以有效的避免延时任务数据的丢失。还可以通过哨兵模式、集群模式有效的避免单点故障造成的服务中断。
缺点
- 需要额外维护redis服务,增加了硬件资源的需求和运维成本。
作者:筱白爱学习!!