JAVA 每天定时任务思路,主要是解耦

      注意:这里不是介绍定时任务怎么用的,是讲解的思路

      我们在日常开发中,我们经常会碰到不断来的汇总任务需求,上周来了一个用户浏览汇总,本周又来了一个用户观看汇总,然后后面有来一大堆汇总任务, 这个。。。 就是有点蛋疼了,每次给我来一个 我就在代码中加一个汇总,并且还会使用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造成内存溢出。

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值