本文核心问题: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
版权声明:本文为博主原创文章,转载请附上博文链接!