1.为什么任务需要存储在数据库中?
延迟任务是一个通用的服务,任何需要延迟得任务都可以调用该服务,需要考虑数据持久化的问题,存储数据库中是一种数据安全的考虑。
2.为什么redis中使用两种数据类型,list和zset?
效率问题,算法的时间复杂度
3.在添加zset数据的时候,为什么不需要预加载?
任务模块是一个通用的模块,项目中任何需要延迟队列的地方,都可以调用这个接口,要考虑到数据量的问题,如果数据量特别大,为了防止阻塞,只需要把未来几分钟要执行的数据存入缓存即可。
搭建一个模块,用于管理任何类型的延迟任务
配置好他的nacos服务
server:
port:
spring:
application:
name:
cloud:
nacos:
discovery:
server-addr:
config:
server-addr:
file-extension:
nacos的yml配置
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:
username:
password:
# 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
# 设置别名包扫描路径,通过该属性可以给包中的类注册别名
type-aliases-package:
准备好数据库
里面有两张表:taskinfo任务信息表和taskinfologs任务信息日志表
相对于java也要创建两个实体类
安装redis
安装在docker或者window,然后用笔记本redis的连接客户端连接
接着就要使用idea连接redis了,先导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redis依赖commons-pool 这个依赖一定要添加 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
nacos里面的yml文件也要配置redis的地址
spring:
redis:
host:
password:
port: 6379
可以先创建一个测试类,测试一下redis
@SpringBootTest(classes = ScheduleApplication.class)
@RunWith(SpringRunner.class)
public class RedisTest {
@Autowired
private CacheService cacheService;
@Test
public void testList(){
//在list的左边添加元素
// cacheService.lLeftPush("list_001","hello,redis");
//在list的右边获取元素,并删除
String list_001 = cacheService.lRightPop("list_001");
System.out.println(list_001);
}
@Test
public void testZset(){
//添加数据到zset中 分值
/*cacheService.zAdd("zset_key_001","hello zset 001",1000);
cacheService.zAdd("zset_key_001","hello zset 002",8888);
cacheService.zAdd("zset_key_001","hello zset 003",7777);
cacheService.zAdd("zset_key_001","hello zset 004",999999);*/
//按照分值获取数据
Set<String> zset_key_001 = cacheService.zRangeByScore("zset_key_001", 0, 8888);
System.out.println(zset_key_001);
}
}
然后创建一个用于接收的类task类,用来传参
然后开始写代码
先实现任务的持久化,传参是的task要要有任务id,类型,优先级 ,executeTime,parameters
,放在数据库中,刚开始存任务里面,我们只需要放数据库的任务信息表和日志表中,日志表的状态就是初始化。
然后就开始放redis中,判断task:
task.getExecuteTime() <= System.currentTimeMillis()或者是否小于当前时间的五分钟之后
前者放入list中,后者放入zset中。
@Service
@Transactional
@Slf4j
public class TaskServiceImpl implements TaskService {
/**
* 添加延迟任务
*
* @param task
* @return
*/
@Override
public long addTask(Task task) {
//1.添加任务到数据库中
boolean success = addTaskToDb(task);
if (success) {
//2.添加任务到redis
addTaskToCache(task);
}
return task.getTaskId();
}
@Autowired
private CacheService cacheService;
/**
* 把任务添加到redis中
*
* @param task
*/
private void addTaskToCache(Task task) {
String key = task.getTaskType() + "_" + task.getPriority();
//获取5分钟之后的时间 毫秒值
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, 5);
long nextScheduleTime = calendar.getTimeInMillis();
//2.1 如果任务的执行时间小于等于当前时间,存入list
if (task.getExecuteTime() <= System.currentTimeMillis()) {
cacheService.lLeftPush(ScheduleConstants.TOPIC + key, JSON.toJSONString(task));
} else if (task.getExecuteTime() <= nextScheduleTime) {
//2.2 如果任务的执行时间大于当前时间 && 小于等于预设时间(未来5分钟) 存入zset中
cacheService.zAdd(ScheduleConstants.FUTURE + key, JSON.toJSONString(task), task.getExecuteTime());
}
}
@Autowired
private TaskinfoMapper taskinfoMapper;
@Autowired
private TaskinfoLogsMapper taskinfoLogsMapper;
/**
* 添加任务到数据库中
*
* @param task
* @return
*/
private boolean addTaskToDb(Task task) {
boolean flag = false;
try {
//保存任务表
Taskinfo taskinfo = new Taskinfo();
BeanUtils.copyProperties(task, taskinfo);
taskinfo.setExecuteTime(new Date(task.getExecuteTime()));
taskinfoMapper.insert(taskinfo);
//设置taskID
task.setTaskId(taskinfo.getTaskId());
//保存任务日志数据
TaskinfoLogs taskinfoLogs = new TaskinfoLogs();
BeanUtils.copyProperties(taskinfo, taskinfoLogs);
taskinfoLogs.setVersion(1);
taskinfoLogs.setStatus(ScheduleConstants.SCHEDULED);
taskinfoLogsMapper.insert(taskinfoLogs);
flag = true;
} catch (Exception e) {
e.printStackTrace();
}
return flag;
}
}
然后数据存储工作准备好了
接着就是消费任务
当消费者过来时,把list任务取出来,
public Task poll(int type, int priority) {
Task task =null;
try {
String key=type+"_"+priority;
String task_json = cacheService.lRightPop(ScheduleConstants.TOPIC + key);
if (StringUtils.isNotBlank(task_json)){
task = JSON.parseObject(task_json, Task.class);
updateDb(task.getTaskId(),ScheduleConstants.EXECUTED);
}
}catch (Exception e){
e.printStackTrace();
log.error("poll task 异常");
}
return task;
}
因为list会消费完,使用zset里面的任务时间过期时,我们就可以把他们的任务同步到list中
这里使用spring的
Scheduled,一分钟执行一次,当部署两台服务时,都去执行refresh定时任务方法 这里不需要,只需要一台执行即可,底层就是使用redis的分布式锁,setnx
然后下面的逻辑就是将每一个符合的zset数据传到list中
@Scheduled(cron = "0 */1 * * * ?")
public void refresh(){
String token = cacheService.tryLock("FUTURE_TASK_SYNC", 1000 * 30);
if (StringUtils.isNotBlank(token)){
log.info("未来数据定时刷新");
Set<String> futureKeys = cacheService.scan(ScheduleConstants.FUTURE + "*");
for (String futureKey : futureKeys) {
String topicKey =ScheduleConstants.TOPIC+futureKey.split(ScheduleConstants.FUTURE)[1];
Set<String> tasks = cacheService.zRangeByScore(futureKey, 0, System.currentTimeMillis());
if (tasks.isEmpty()){
cacheService.refreshWithPipeline(futureKey,topicKey,tasks);
log.info("未来数据刷新");
}
}
}
trylock方法
redis的管道传输
接着写一个openfeign接口来对应上这个计划模块
在计划模块实现openfeign,来接收openfeign的远程调用
然后,我们需要在我们想要实现延时任务的地方,整理好参数,传给openfeign将任务添加到数据库等等,openfeign传入计划模块,实现插入数据库,插入redis中。这样,我们的任务就上传到redis中了,业务就完成了,剩下的任务怎么消费,就交给后台
这里我们用
y@Scheduled(fixedRate = 1000)这个每秒执行一次来执行任务
因为这个模块时负责定时审核文章,查询任务时,只需要按照list的任务名称来查询,查询出来的值里放着参数时文章的id,然后调用审核文章的接口就完成了定时审核
。
后面还有一个,因为如果执行时间大于五分钟之后,就不放入zset了,防止阻塞。所以我们只需要弄个定时上传即可
先清理redis中的所有数据,简单粗暴,然后在查询任务信息表,插入redis就同步了redis和数据库
@Scheduled(cron = "0 */5 * * * ?")
@PostConstruct//spring启动时就会执行一次这个方法
public void reloadData() {
clearCache();
log.info("数据库数据同步到缓存");
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, 5);
//查看小于未来5分钟的所有任务
List<Taskinfo> allTasks = taskinfoMapper.selectList(Wrappers.<Taskinfo>lambdaQuery().lt(Taskinfo::getExecuteTime,calendar.getTime()));
if(allTasks != null && allTasks.size() > 0){
for (Taskinfo taskinfo : allTasks) {
Task task = new Task();
BeanUtils.copyProperties(taskinfo,task);
task.setExecuteTime(taskinfo.getExecuteTime().getTime());
addTaskToCache(task);
}
}
}
private void clearCache(){
// 删除缓存中未来数据集合和当前消费者队列的所有key
Set<String> futurekeys = cacheService.scan(ScheduleConstants.FUTURE + "*");// future_
Set<String> topickeys = cacheService.scan(ScheduleConstants.TOPIC + "*");// topic_
cacheService.delete(futurekeys);
cacheService.delete(topickeys);
}
总之,redis延时任务业务流程就是,把你需要延时的任务放入redis中,然后写一个消费方法定时处理redis的list中的方法
我学习的项目是在发布文章的流程里,把审核文章删掉,也就是实现一个定时发布,,接着调用新开的一个方法,用来特意整理数据来调用openfeign,来调用计划模块,将任务(任务里有我们的文章id,当用户点击发布时,我们会将文章保存在自媒体的文章表中,前台时看不到的)放入数据库和redis中,这样只要定时文章的时间小于当前时间,就会立马被处理,因为消费任务的逻辑时间是写了每一秒执行