注意:这里不是介绍定时任务怎么用的,是讲解的思路
我们在日常开发中,我们经常会碰到不断来的汇总任务需求,上周来了一个用户浏览汇总,本周又来了一个用户观看汇总,然后后面有来一大堆汇总任务, 这个。。。 就是有点蛋疼了,每次给我来一个 我就在代码中加一个汇总,并且还会使用spring 的定时任务,当来了N个汇总任务,汇总代码里面就乱糟糟的。而且每一个汇总,都得考虑分布式,要不然在分布式架构上跑,谁知道会出现什么天文数字。 哈哈哈,被坑过的人 评论打个1
设计思路: 采用任务方式(Mysql)和策略模式(Java)去实现
为了解决这些问题,我结合自己开发经验,总结了一套代码,能有效解决源源不断的汇总任务.下面我就开始介绍
下面开始介绍下主要的表: task_cron
CREATE TABLE `task_cron` (
`id` int(8) NOT NULL AUTO_INCREMENT,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新任务时间',
`type` varchar(20) DEFAULT NULL COMMENT '汇总任务标识',
`summary_time` date NOT NULL COMMENT '汇总天数',
`num` int(8) DEFAULT NULL COMMENT '执行次数',
`state` varchar(255) NOT NULL COMMENT 'PENDING:正在汇总 COMPLETE:完成 UPDATED : 重新汇总 ',
PRIMARY KEY (`id`),
UNIQUE KEY `uq_summary` (`summary_time`,`type`) USING BTREE COMMENT '汇总时间,汇总任务'
) ENGINE=InnoDB DEFAULT CHARSET=utf8
表的字段如上,我们关注的点在于是summary_time,type,num,state 这四个字段。
summary_time: 标识当前汇总天数,当前汇总到那天来了,如果没有汇总任务 则从昨天开始
type:类型,针对于不同的汇总任务,比如按用户汇总,按订单汇总,只要有汇总需求来,就往这个里面加属性
num: 这个任务执行了几次,默认有次数上限, 我一般用于限制任务执行次数。
state: 汇总有三个状态,你也可以自己增加,比如删除汇总任务状态等。 我这里列举了三种,处理中,完成,重新汇总
介绍完表后,大家应该都有一定的眉目吧。 如果在java 定时任务中,进行实现呢。 我们的要求是,代码可阅读性高,耦合度低。
定时任务java代码如下
package cn.ibabygroup.statistic.cron;
import cn.ibabygroup.statistic.service.TaskCronService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import java.time.Instant;
/**
* 定时任务 后续的任务 都可以放入进来
*/
@Configuration //1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling // 2.开启定时任务
@Slf4j
public class TaskQueueCron {
@Autowired
private TaskCronService taskCronService;
/**
* 直播学习统计定时任务
*/
@Scheduled(cron = "0 0/30 * * * ?") //30分钟执行一次
// @Scheduled(cron = "0/5 * * * * *?")
public void liveStudentTimeCron(){
try {
log.info("{} 执行liveStudentTimeCron 汇总任务", Instant.now());
taskCronService.summaryLiveStudnt();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 处理PENDING的
*/
@Scheduled(cron = "0 0/15 * * * ?") //15分钟执行一次
// @Scheduled(cron = "1 * * * * *?")
public void pendingTask(){
log.info("{} 执行PENDING 汇总任务", Instant.now());
taskCronService.taskPending();
}
/**
* 处理Updated的
*/
@Scheduled(cron = "0 0/15 * * * *?") //15分钟执行一次
// @Scheduled(cron = "1 * * * * ?")
public void updatedTask(){
log.info("{} 执行updated 汇总任务", Instant.now());
taskCronService.taskUpdated();
}
}
这个类是定时任务,代码里面只开启了三个定时任务. 针对于表中不同类型的实现,这里都能一一对应。 三种类型,各管个的,互不打扰。
下面介绍下TaskCronService
package cn.ibabygroup.statistic.service;
import cn.ibabygroup.statistic.dao.TaskCronMapper;
import cn.ibabygroup.statistic.dto.TaskCron;
import cn.ibabygroup.statistic.dto.enums.State;
import cn.ibabygroup.statistic.dto.enums.TaskCronType;
import cn.ibabygroup.statistic.utils.DateUtils;
import cn.ibabygroup.statistic.utils.RedisKeyUtils;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
/**
* 汇总任务
* 后续有添加的 只需要实现数据拉去和插入
*/
@Slf4j
@Service
public class TaskCronService {
private TaskCronMapper taskCronMapper;
private LiveStudntService liveStudntService;
private OrderService orderService;
private RedisService redisService;
//处理失败的任务
Map<TaskCronType ,Function<TaskCron,Boolean> > pendingsMaps = Maps.newHashMap();
Map<TaskCronType ,Function<TaskCron,Boolean> > updatedMaps = Maps.newHashMap();
@Autowired
public TaskCronService(TaskCronMapper taskCronMapper,LiveStudntService liveStudntService,OrderService orderService,RedisService redisService){
this.taskCronMapper = taskCronMapper;
this.liveStudntService = liveStudntService;
this.orderService = orderService;
this.redisService = redisService;
pendingsMaps.put(TaskCronType.LIVEUSER,liveStudntService.doLiveTimeSummary());
pendingsMaps.put(TaskCronType.INCOME,orderService.doOrderSummary(true));
updatedMaps.put(TaskCronType.INCOME,orderService.doOrderSummary(false));
}
public void summaryLiveStudnt(){
pendingsMaps.entrySet().forEach(x->{
summaryTaskCron(x.getKey(),x.getValue());
});
}
public void summaryTaskCron(TaskCronType taskCronType, Function<TaskCron,Boolean> function){
TaskCron taskCron = taskCronMapper.findByOrderTime(taskCronType.name());
try{
String yesterday;
if( ObjectUtils.isEmpty(taskCron) ){
//空从昨天开始处理
taskCron = new TaskCron();
yesterday = DateUtils.getYesterday();
}else{
if( taskCron.getState() == State.PENDING || taskCron.getState() == State.UPDATED){
log.info("{},{}正在汇总种,请等待",taskCron.getType(),taskCron.getSummaryTime());
return;
}
LocalDate localDate = DateUtils.stringToLocalDate(taskCron.getSummaryTime());
//在今天或者今天之后,不运行汇总
if(!LocalDate.now().isAfter(localDate)){
return;
}
yesterday = DateUtils.getLocalDateToString(localDate);
}
//创建当前新任务
TaskCron toTask = initTaskCron(taskCronType,yesterday);
taskQueueStart(toTask,function);
}catch (Exception e){
log.info("{},{} Task定时任务出错,出错原因:{}",taskCron.getSummaryTime(),taskCron.getType(),e);
throw e;
}
}
private TaskCron initTaskCron(TaskCronType type,String yesterday){
TaskCron taskCron = new TaskCron();
taskCron.setState(State.PENDING);
taskCron.setSummaryTime(yesterday);
taskCron.setType(type);
taskCron.setExecuNum(0);
return taskCron;
}
/**
* 任务开始模块,当有新的任务开始的时候,调用此方法,另外再pendingsMaps 需要put执行方法块 保证任务中断后,会继续汇总
* @param taskCron
* @param function
*/
public void taskQueueStart(TaskCron taskCron,Function<TaskCron,Boolean> function){
int i ;
if( (i = taskCronMapper.insert(taskCron) ) == 0 ){
//当前有任务在执行
return;
}
try{
//方法内部决定是否回滚数据,这边只解决任务队列不会回滚,有PENDING 队列去再次汇总
if (function.apply(taskCron)){
taskCron.setExecuNum(taskCron.getExecuNum()+1);
taskCron.setState(State.COMPLETE);
i += taskCronMapper.update(taskCron);
if(i==2){
log.info("{}:{} 汇总完成",taskCron.getSummaryTime(),taskCron.getType());
return;
}
log.info("{}:{} 汇总失败,汇总任务未执行",taskCron.getSummaryTime(),taskCron.getType());
}
}catch (Exception e){
log.info("{}:{} 汇总错误,汇总任务未执行",taskCron.getSummaryTime(),taskCron.getType(),e);
}
}
/**
* 开始处理PENDING
*/
public void taskPending(){
List<TaskCron> list = taskCronMapper.findByPending();
//按照顺序执行 如果处理一个失败
list.forEach(this::doPendingTask);
}
public void taskUpdated(){
List<TaskCron> list = taskCronMapper.findByUpdated();
list.forEach(this::doUpdatedTask);
}
@Transactional
public void doPendingTask(TaskCron x){
try{
if(x.getExecuNum() > 3){
log.info("{}:{} 已执行{}",x.getType(),x.getSummaryTime(),x.getExecuNum());
}
Function<TaskCron,Boolean> function = pendingsMaps.get(x.getType());
if(function == null){
log.info("{}:{} 无处理PENDING方法",x.getType(),x.getSummaryTime());
return;
}
Boolean result = function.apply(x);
if(!result){
//考虑中止循环 可能存在却一天的数据
return;
}
//更新Task任务
x.setExecuNum(x.getExecuNum()+1);
x.setState(State.COMPLETE);
int i = taskCronMapper.update(x);
if(i == 0){
log.error("{}:{} 更新Task任务表失败",x.getType(),x.getSummaryTime());
throw new NullPointerException(x.getType()+x.getSummaryTime()+"更新Task任务表失败");
}
log.info("{}:{} 处理PENDING 成功",x.getType(),x.getSummaryTime());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}catch (Exception e){
log.info("{},{} Task PENDING 定时任务出错,出错原因:{}",x.getSummaryTime(),x.getType(),e);
throw e;
}
}
public void doUpdatedTask(TaskCron x){
String redisLock = RedisKeyUtils.getTaskCronTypeTime(x.getType().name(),x.getSummaryTime());
try{
//加锁 update 减少重复汇总次数 默认加锁60秒
if(!redisService.lock(redisLock)){
return;
}
Function<TaskCron,Boolean> function = updatedMaps.get(x.getType());
if(function == null){
log.info("{}:{} 无重新汇总方法",x.getType(),x.getSummaryTime());
return;
}
log.info("{}:{} 重新新汇总",x.getType(),x.getSummaryTime());
Boolean result = function.apply(x);
if(!result){
//考虑中止循环
return;
}
//更新Task任务
x.setExecuNum(x.getExecuNum()+1);
x.setState(State.COMPLETE);
int i = taskCronMapper.update(x);
if(i == 0){
log.error("{}:{} 更新Task任务表失败",x.getType(),x.getSummaryTime());
throw new NullPointerException(x.getType()+x.getSummaryTime()+"更新Task任务表失败");
}
log.info("{}:{} 重新新汇总 成功",x.getType(),x.getSummaryTime());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}catch (Exception e){
log.info("{},{} Task UPDATED 定时任务出错,出错原因:{}",x.getSummaryTime(),x.getType(),e);
throw e;
}finally {
redisService.del(redisLock);
}
}
}
Mybatis XML文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.ibabygroup.statistic.dao.TaskCronMapper">
<insert id="insert" parameterType="cn.ibabygroup.statistic.dto.TaskCron">
INSERT IGNORE task_cron(type,summaryTime,state)
VALUES( #{type},#{summaryTime},#{state})
</insert>
<update id="update" parameterType="cn.ibabygroup.statistic.dto.TaskCron">
UPDATE task_cron SET state = #{state} , execuNum = #{execuNum} WHERE summaryTime = #{summaryTime} and type = #{type}
</update>
<select id="findByTypeTime" parameterType="cn.ibabygroup.statistic.dto.TaskCron" resultType="cn.ibabygroup.statistic.dto.TaskCron">
SELECT id,type,date_format( summaryTime, '%Y-%m-%d' ) as summaryTime ,state,execuNum
FROM task_cron
WHERE summaryTime = #{summaryTime} AND type = #{type}
</select>
<select id="findByOrderTime" parameterType="java.lang.String" resultType="cn.ibabygroup.statistic.dto.TaskCron">
SELECT id,type,date_format( summaryTime, '%Y-%m-%d' ) as summaryTime,state,execuNum
FROM task_cron
WHERE type = #{type}
ORDER BY summaryTime desc
limit 1
</select>
<select id="findByPending" resultType="cn.ibabygroup.statistic.dto.TaskCron">
SELECT id,type,date_format( summaryTime, '%Y-%m-%d' ) as summaryTime,state,execuNum
FROM task_cron
WHERE state = 'PENDING'
AND now() >SUBDATE(createTime,interval 15 DAY)
order by summaryTime
limit 20
</select>
<select id="findByUpdated" resultType="cn.ibabygroup.statistic.dto.TaskCron">
SELECT id,type,date_format( summaryTime, '%Y-%m-%d' ) as summaryTime,state,execuNum
FROM task_cron
WHERE state = 'UPDATED'
AND now() >SUBDATE(createTime,interval 15 DAY)
order by summaryTime
limit 20
</select>
</mapper>
这个里面的代码 没有一点业务逻辑代码,全是实现的Task分发任务,针对于每个任务的处理.
大家看到代码里面并没解决分布式汇总, 代码处只有update是实现分布式,其他都是在业务逻辑代码里面的分布式,那么我是怎么实现的。 我是利用mysql的唯一索引在加 INSERT IGNORE 或者 replace into 这两种插入方式(注意 都需要唯一索引),并不用加锁。 这个看你们实际的业务量,如果每天的业务量比较大,那么可以加一把redis 锁。问题不大
实际上 我是利用mysql 表做了一个任务表,然后在jdk1.8的函数编程来实现解耦,具体实现的逻辑,交给当时写代码的人。
后续对于扩展很容易,我们在做一个功能,对这张表的数据增删查找,然后就可以动态的控制汇总任务。
注意:state在UPDATED 下,我是用replace into 替换原有的数据。
其实这里 还可以用其他的方式,不一定用策略模式,还可以用工厂呢,这些模式。个人有点偷懒,没有把策略模式独立出去。
后期优化下,改动起来要花点时间。
Spring 获取的对象是单列,不用担心Map造成内存溢出。