掌柜大作战(7):Spring中配置定时任务,封装任务执行流程;同一时刻只让一台机器执行,尽可能避免并发和并行,避免任务数据被处理2次

版权声明:襄阳雷哥的版权声明 https://blog.csdn.net/FansUnion/article/details/83654243

本文核心问题:Spring中配置定时任务,封装任务执行流程;同一时刻只让一台机器执行,尽可能避免并发和并行,避免任务数据被处理2次。

项目中,基本都会存在一些后台性质的工作,可以用定时任务搞定。
Spring中配置定时任务,个人倾向使用Spring自带的Task配置,不用引入新的技术点,简单的项目足够了。
1、Spring定时任务配置 spring-worker.xml
<!-- 0 0/1 * * * ?  1分钟执行1次-->
    <!-- 0 0 5 * * ?  凌晨5点执行-->
    <!-- 0 0 */1 * * ?  每小时执行1次-->
    <bean id="blacklistTimeoutTask" class="com.jd.cav.web.task.BlacklistTimeoutTask" />
    <task:scheduled-tasks>
        <task:scheduled ref="blacklistTimeoutTask" method="run"
            cron="0 0 */1 * * ?" />
    </task:scheduled-tasks>

京东内部,习惯把“定时任务”叫做worker,我个人习惯spring-task.xml这种名字。

2、普通的任务配置
public class BlacklistTimeoutTask {
 
    public void run(){
       //code
   }
 
}


3、任务流程封装
在实际开发过程中,发现很多项目的定时任务执行,符合一定的流程,于是定义了一个父类CronTask,封装了任务执行的流程。
/**
 * 定时任务模型。<br/>
 * 约定1个任务的处理流程:<br/>
 * 1、是否有“权限”或“锁”执行;<br/>
 * 2、查询有哪些数据需要处理;<br/>
 * 3、for循环,开启新线程,包装上下文,执行1个任务;<br/>
 * 4、结束任务,释放“权限”或“锁”。<br/>
 *
 */

import java.util.List;
 
import javax.annotation.Resource;
 
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.scheduling.SchedulingTaskExecutor;
 
public abstract class CronTask<T> {
    private Logger logger = Logger.getLogger(getClass());
    @Resource
    private TaskCheckService taskCheckService;
 
    /**
     * 任务调度的入口
     */
    public void run() {
        // 第1步
        final String taskKey =getTaskKey();
        if(StringUtils.isEmpty(taskKey)){
            throw new RuntimeException("The taskKey is null or empty");
        }
        logger.info(String.format("The task %s start",taskKey));
        //如果只让1台机器执行,需要判断是否有锁
        boolean canDoTask = true;
        if(mustOneMarchine()){
            Long timeout = lockTimeoutMiliSeconds() ;
            canDoTask=taskCheckService.startTask(taskKey,timeout );
        }
        
        if (!canDoTask) {
            logger.info(String.format("The task %s does not get lock,exit.", taskKey));
            return;
        }
        try {
            // 第2步 查询任务列表
            List<T> taskList = findTaskList();
            // 第3步 执行任务,检查是否为空,是否需要多线程
            if (CollectionUtils.isNotEmpty(taskList)) {
                logger.info(String.format("taskKey=%s,taskSize=%s",taskKey,taskList.size()));
                // 逐个执行
                for (final T task : taskList) {
                    SchedulingTaskExecutor executor = getExecutor();
                    if(executor == null){
                        logger.info(String.format("Do task in the main thread,taskKey=%s", taskKey));
                        doOneTask(task);
                    }else{
                        logger.info(String.format("Do task by thread pool,taskKey=%s", taskKey));
                        executor.execute(new Runnable(){
 
                            @Override
                            public void run() {
                                logger.info(String.format("Do task by thread pool start,taskKey=%s", taskKey));
                                doOneTask(task);
                                logger.info(String.format("Do task by thread pool end,taskKey=%s", taskKey));
                            }
                            
                        });
                    
                    }
                    
                }
            }else{
                logger.info(String.format("The task %s size is 0,exit",taskKey));
            }
        } catch (Exception e) {
            logger.error(e);
        } finally {
            // 第4步
            taskCheckService.endTask(taskKey);
        }
        logger.info(String.format("The task %s end",taskKey));
    }
    
    /**
     * 查询任务列表
     * @return 任务列表
     */
    protected abstract List<T> findTaskList();
    /**
     * 执行1个任务
     * @param task 将要执行的任务
     */
    protected abstract void doOneTask(T task);
    /**
     * 任务的key,最好是唯一的
     * @return 任务的key
     */
    protected abstract String getTaskKey();
    /**
     * 默认不需要多线程,如果需要多线程,重载此方法,返回SchedulingTaskExecutor的1个实例,比如org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
     * @return 线程池
     */
    protected  SchedulingTaskExecutor getExecutor(){
        return null;
    }
    
    /**
     * 默认只让1台机器执行,但是数据库task_config表必须先插入1条key-value记录
     * @return 是否只让1台机器执行
     */
    protected boolean mustOneMarchine(){
        return true;
    }
    
    /**
     * 定时任务占居锁的最长时间
     * @return
     */
    protected Long lockTimeoutMiliSeconds(){
        return TaskCheckService.DEFAULT_TIMEOUT_MILISECONDS;
    }
}


4、具体的任务BlacklistTimeoutTask
继承父类CronTask,重写3个方法。
findTaskList:查找任务列表
doOneTask:处理具体的1个Task
getTaskKey:这个任务的唯一标记

如果想让多个任务同时执行,重写getExecutor方法。
public class BlacklistTimeoutTask extends CronTask<Blacklist> {
    private Logger logger = Logger.getLogger(getClass());

    @Resource
    private BlacklistService blacklistService;

    @Resource
    private ThreadPoolTaskExecutor coreTaskExecutor;

    @Override
    protected List<Blacklist> findTaskList() {
        return blacklistService.listAllTimeout();
    }

    @Override
    protected void doOneTask(Blacklist blacklist) {
        Integer id = blacklist.getId();
        try {
            blacklistService.remove(id);
        } catch (Exception e) {
            logger.error(e);
        }

    }

    @Override
    protected String getTaskKey() {
        return TaskKeyConsts.task_blacklist_timeout_running;
    }

    @Override
    protected SchedulingTaskExecutor getExecutor() {
        return coreTaskExecutor;
    }
}

5、Spring中多线程配置
spring-info-threadpool.xml
    <!-- spring线程池,执行关键任务 -->
    <bean id="coreTaskExecutor"
        class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
        <!-- 线程池维护线程的最少数量 -->
        <property name="corePoolSize" value="5" />
        <!-- 线程池维护线程的最大数量 -->
        <property name="maxPoolSize" value="20" />
        <!-- 缓存队列 -->
        <property name="queueCapacity" value="1000" />
        <!-- 允许的空闲时间 -->
        <property name="keepAliveSeconds" value="1800" />
        <!-- 对拒绝task的处理策略 -->
        <property name="rejectedExecutionHandler">
            <bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" />
        </property>
    </bean>

6、Spring中异步线程和注解@Async
之前配置@Async没有生效:少了xml配置或内部方法调用。
因此,这个注解配置异步线程很鸡肋,还不如自己配置多线程bean,注入到项目中,手动启动新线程来执行。
  <bean id="asyncTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
        <!-- 核心线程数  -->  
        <property name="corePoolSize" value="10" />  
        <!-- 最大线程数 -->  
        <property name="maxPoolSize" value="50" />  
        <!-- 队列最大长度 >=mainExecutor.maxSize -->  
        <property name="queueCapacity" value="10000" />  
        <!-- 线程池维护线程所允许的空闲时间 -->  
        <property name="keepAliveSeconds" value="300" />  
        <!-- 线程池对拒绝任务(无线程可用)的处理策略 -->  
        <property name="rejectedExecutionHandler">  
          <bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" />  
        </property>  
    </bean>
      <!-- 基于类生成代理类,共用线程池,扫描Async注解 -->
    <task:annotation-driven proxy-target-class="true" executor="asyncTaskExecutor"></task:annotation-driven>

7、同一时刻只让一台机器执行,尽可能避免并发和并行
目前做过的项目,数据量都不算大,1台机器10分钟就可以把所有该做的任务处理完成。
另外,线上机器部署2台服务,2台机器时间总体一致,同一时刻1个定时任务会执行2次,这样必须保证2台机器处理的数据是不同的。如果2个任务处理了同样的数据,只能有1个成功,意味着会浪费1半的执行。
因此,个人习惯于使用悲观锁,让1个任务执行处理就可以了。任务处理完成,标记相关任务标志成功,下一次任务就不会再查询到了。
另外,就算是1个任务执行,也还是可能出现并发的情况。
a.任务中使用多线程。大部分情况不需要,但有的时候,出于政治因素,非要使用多线程。
b.定时处理1个任务数据的时候,可能有其它异步任务也再修改这个数据。
比如,京东内部消息服务JMQ。
c.前台用户或后台用户,恰好也修改了这个任务相关的数据。
因此,要么使用悲观锁,修改之前先抢占锁。要么悲观锁,让某一个修改失败。

具体到2个任务,只让其中1个执行,采用的是“悲观锁”方式实现。
每个任务在数据库有1条任务记录,执行的时候“事务+select for update”判断是否能执行。
如代码所示,封装了startTask和endTask方法。

import java.util.Date;
 
import javax.annotation.Resource;
 
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.transaction.annotation.Transactional;
 
import com.jd.cav.dao.ConfigMapper;
import com.jd.cav.domain.Config;
public class TaskCheckService {
    /**
     * 默认超时时间为50分钟,定时任务通常1个小时1次就足够了
     */
    public static final Long DEFAULT_TIMEOUT_MILISECONDS = 1 * 50 * 60 * 1000L;
    private static Logger logger = Logger.getLogger(TaskCheckService.class);
 
    static final String TASK_RUNNING_NO = "0";
    static final String TASK_RUNNING_YES = "1";
    @Resource
    private ConfigMapper configMapper;
    
    // 事务+forUpdate
    @Transactional
    // 可以执行任务,就返回config配置
    public boolean startTask(String key,Long timeout) {
        logger.info(String.format("Try to get taskLock,key=%s,start",key));
        // 简单锁1行,防止多个任务同时执行
        Config config = configMapper.getForUpdate(key);
        if (config == null) {
            logger.error("The task config is null,key=" + key);
            return false;
        }
        boolean canDoTask = canDoTask(key, config,timeout);
        if (canDoTask) {
            // 开始执行任务,标记为1
            config.setValue(TaskCheckService.TASK_RUNNING_YES);
            config.setTime(new Date());
            configMapper.update(config);
        }
        logger.info(String.format("Try to get taskLock,key=%s,end",key));
        return canDoTask;
    }
 
    @Transactional
    public void endTask(String key) {
        logger.info(String.format("Try to reback taskLock,key=%s,start",key));
        Config config = configMapper.getForUpdate(key);
        // 任务结束,标记为0
        config.setValue(TaskCheckService.TASK_RUNNING_NO);
        config.setTime(new Date());
        configMapper.update(config);
        logger.info(String.format("Try to reback taskLock,key=%s,end",key));
    }
    
    private static boolean canDoTask(String key, Config config,Long timeout) {
        if(timeout == null){
            timeout = DEFAULT_TIMEOUT_MILISECONDS;
        }
        // 有任务正在执行
        if (StringUtils.equals(config.getValue(), TaskCheckService.TASK_RUNNING_NO)) {
            return true;
        } else if (StringUtils.equals(config.getValue(), TaskCheckService.TASK_RUNNING_YES)) {
            // 超过1小时,产生了“死锁”,正常执行任务
            Date lastUpdateTime = config.getTime();
            Date nowTime = new Date();
            long timeResult = nowTime.getTime() - lastUpdateTime.getTime();
            boolean isTimeout = timeResult > timeout;
            if (isTimeout) {
                logger
                        .error("Has a task is running timeout,exit,task key=" + key);
                return true;
            } else {
                return false;
            }
        }
        return true;
    }
}

任务配置数据库表
CREATE TABLE `task_config` (
  `name` varchar(50) NOT NULL DEFAULT '' COMMENT '参数的名字',
  `value` varchar(50) NOT NULL DEFAULT '0' COMMENT '参数的值',
  `time` datetime DEFAULT NULL COMMENT '上次更新时间',
  `yn` int(11) NOT NULL DEFAULT '1' COMMENT '是否有效',
  PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='系统参数配置和运行状态';

另外,为了防止“死锁”,根据上次更新时间time和现在时间比较。如果value=1表示在执行,但是已经执行了50分钟以上,仍然判定任务可以执行。

小雷FansUnion-京东程序员一枚
2017年10月
北京-亦庄
--------------------- 
作者:小雷FansUnion 
来源:CSDN 
原文:https://blog.csdn.net/FansUnion/article/details/78347207 
版权声明:本文为博主原创文章,转载请附上博文链接!

猜你喜欢

转载自blog.csdn.net/FansUnion/article/details/83654243
今日推荐