目录
介绍
一、 通过Redis的方式实现延时任务实现
定义延时任务调度器
具体的业务Handler封装
注册具体的Handler
二、使用云调度平台方式实现延时任务
延时任务的表结构
延时任务Service的实现
介绍
延时任务,一个非常常用和常见的技术组件:如订单下单后五分钟发邮件提醒用户过来查看,又或者定时任务失败后3分钟延时任务自动重试。延时任务的概念很简单,但是实现起来方法很多,下面来介绍一下平常项目用的两种。
一、 通过Redis的方式实现延时任务实现
实现说明:
- 注册延时任务:将具体的taskid和任务的参数还有执行时间保存在Redis
- 服务启动的时候会起一个轮询,每秒去轮询Redis提取出需要执行的延时任务
- 根据延时任务的taskId获取到具体的Handler处理器类型
- 根据Handler处理器的类型去获取内存中已经注册好的Handler去实现具体的业务
定义延时任务调度器
调度器接口类:
public interface DelayJobScheduleService {
/**
* 提交定时任务
*
* @param delayJob 提交定时任务
* @return
*/
boolean submitJob(DelayJob delayJob);
}
调度器具体实现(内附注释):
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.StrUtil;
import com.test.common.until.CompareUtils;
import com.test.common.until.GsonUtil;
import com.test.common.until.JacksonUtils;
import com.test.infra.delay.handler.DelayJobHandler;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gson.reflect.TypeToken;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.skywalking.apm.toolkit.trace.Trace;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
/**
* @date @date 11/23/2021 4:14 下午
*/
@Slf4j
@Component
@Order(200)
public class RedisDelayJobScheduleServiceImpl implements DelayJobScheduleService {
@Resource
private DelayJobStrategy delayJobStrategy;
@Resource
private StringRedisTemplate stringRedisTemplate;
private static final String DELAY_JOB_QUEUE_KEY = "delayJob::queue";
private static String DELAY_JOB_BODY_KEY_PREFIX = "delayJob::body";
private static final ScheduledExecutorService SINGLE_THREAD_EXECUTORS;
static {
String threadNameFormat = "delay-job-thread";
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat(threadNameFormat).build();
SINGLE_THREAD_EXECUTORS = Executors.newSingleThreadScheduledExecutor(namedThreadFactory);
}
/**
* 定时任务提交到Redis
* @param delayJob 提交定时任务
*/
@Override
public boolean submitJob(DelayJob delayJob) {
if (delayJob == null) {
log.info("job is empty!");
return false;
}
String msgContent = GsonUtil.toJsonString(delayJob.getMsgContent());
String taskId = genTaskId(delayJob.getType());
long currentTimeMillis = System.currentTimeMillis();
long score = currentTimeMillis + delayJob.getDelayDuration();
stringRedisTemplate.opsForZSet().add(DELAY_JOB_QUEUE_KEY, taskId, score);
stringRedisTemplate.opsForValue().set(taskId, msgContent);
log.info("submit delayJob:{} success! taskId:{} currentTimeMillis:{} score:{}", GsonUtil.toJsonString(delayJob), taskId, currentTimeMillis, score);
return true;
}
/**
* 生成TaskId
*
* @param delayMsgType
* @return
*/
private String genTaskId(DelayMsgType delayMsgType) {
return Joiner.on(StrUtil.UNDERLINE)
.join(DELAY_JOB_BODY_KEY_PREFIX,
//消息类型
delayMsgType.getCode(),
UUID.randomUUID().toString());
}
private DelayMsgType parseDelayMsgType(String taskId) {
String delayMsgTypeCode = Splitter.on(StrUtil.UNDERLINE).splitToList(taskId).get(1);
return DelayMsgType.getDelayMsgType(Integer.valueOf(delayMsgTypeCode));
}
/**
* 服务启动时开启调度
*/
@PostConstruct
public void schedule() {
ZSetOperations<String, String> zSetOperations = stringRedisTemplate.opsForZSet();
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
// 每一秒轮询
SINGLE_THREAD_EXECUTORS.scheduleWithFixedDelay(new Runnable() {
@Override
@Trace
public void run() {
// 异常处理, 只打印日志
Thread.currentThread().setUncaughtExceptionHandler((t1, e) -> {
log.error("execute delay job fail", e);
});
log.info("schedule delayJob");
DelayJobHandler delayJobHandler = null;
long score = System.currentTimeMillis();
// 去除满足条件的延时任务
Set<String> taskIdSet = zSetOperations.rangeByScore(DELAY_JOB_QUEUE_KEY, 0, score, 0, 1);
if (CollectionUtils.isEmpty(taskIdSet)) {
return;
}
log.info("delayJob,taskIds: {}", JacksonUtils.toJsonStr(taskIdSet));
for (String taskId : taskIdSet) {
if (CompareUtils.gt(zSetOperations.remove(DELAY_JOB_QUEUE_KEY, taskId), 0L)) {
String msgContent = valueOperations.get(taskId);
log.info("begin execute delayJob, taskId:{} msgContent:{},score:{}", taskId, msgContent, score);
DelayMsgType delayMsgType = parseDelayMsgType(taskId);
delayJobHandler = delayJobStrategy.getByType(delayMsgType);
if (delayJobHandler != null) {
delayJobHandler.handle(GsonUtil.toJavaBean(msgContent, TypeToken.get(delayMsgType.getParamClazz())));
}
stringRedisTemplate.delete(taskId);
log.info("finished execute delayJob,taskId:{}", taskId);
}
break;
}
}
}, 1, 1, TimeUnit.SECONDS);
}
}
具体的业务Handler封装
先来一个handler的抽象接口类:
public interface DelayJobHandler<T> {
/**
* 处理延时任务
*
* @param message
*/
void handle(T message);
/**
* 类型
*
* @return
*/
DelayMsgType delayMsgType();
}
定义延时任务的类型枚举
public enum DelayMsgType {
ORDER_NOTIFY(1, "订单邮件提醒", PoNotifyDTO.class),
RETRY_JOB(2, "定时任务失败重试", RetryDto.class),
;
/**
* 值
*/
private final Integer code;
/**
* 描述
*/
private final String desc;
/**
* 这个是你需要在延时任务传递的参数
*/
private final Class paramClazz;
public Integer getCode() {
return code;
}
public String getDesc() {
return desc;
}
DelayMsgType(Integer code,
String desc,
Class paramClazz) {
this.code = code;
this.desc = desc;
this.paramClazz = paramClazz;
}
public static DelayMsgType getDelayMsgType(Integer code) {
for (DelayMsgType type : DelayMsgType.values()) {
if (type.code.equals(code)) {
return type;
}
}
return null;
}
public Class getParamClazz() {
return paramClazz;
}
实现处理器,实现Handler封装接口:
@Slf4j
@Component
public class GenPoRetryHandler implements DelayJobHandler<RetryDto> {
@Override
public void handle(RetryDto message) {
// 延时任务具体处理的业务逻辑
.......
}
@Override
public DelayMsgType delayMsgType() {
// 返回具体的类型枚举
return DelayMsgType.RETRY_JOB;
}
}
注册具体的Handler
@Slf4j
@Component
public class DelayJobStrategy implements ApplicationContextAware {
private Map<DelayMsgType, DelayJobHandler> strategyMap = new ConcurrentHashMap<>();
public DelayJobHandler getByType(DelayMsgType delayMsgType) {
return strategyMap.get(delayMsgType);
}
@Override
public void setApplicationContext(ApplicationContext atx) throws BeansException {
this.setBeanMap(atx, strategyMap);
}
private void setBeanMap(ApplicationContext atx,
Map<DelayMsgType, DelayJobHandler> beanMap) {
String[] beanNames = atx.getBeanNamesForType(DelayJobHandler.class);
for (String beanName : beanNames) {
DelayJobHandler bean = atx.getBean(beanName, DelayJobHandler.class);
DelayMsgType delayMsgType = bean.delayMsgType();
beanMap.put(delayMsgType, bean);
}
if (log.isDebugEnabled()) {
log.info("load {} beanMap success! \n beanMap detail:{}",
DelayJobHandler.class.getSimpleName(),
JSON.toJSONString(beanMap, SerializerFeature.WriteMapNullValue));
}
}
}
二、使用云调度平台方式实现延时任务
整体设计如上图所示,左边绿色的区域我们可以借助云平台的轻函数无服务serverless能力实现分钟级调度,例如腾讯云的:
或者云产品的分布式调度平台,如:
我们只需要在云平台配置分钟级的调度,然后在具体的业务服务里面实现延时任务的状态流转 / 执行 / 提交,将延时任务的调度和延时任务的管理解耦,这样的好处
1. 我们可以对延时任务管控起来,每个延时任务的状态,传参,执行时间,成功或失败,失败重试都变得清晰可见
2. 可以线上化配置我们的job调度时间,更改调度间隔,不需要重新去发版。
延时任务的表结构
任务表字段
id、type、handler、status、tigger_time、update_time、create_time
延时任务Service的实现
具体的时序图如下:
下面我们来实战一下,先定义接口:
public interface DelayJobScheduleService {
/**
* 延迟处理
*/
void handleDelayJob();
/**
* 处理中断的延时任务
*/
void handleAbnormalDelayJob();
/**
* 添加延时任务
*/
void addJob(List<TriggerTaskInfo> triggerTasks);
}
我们先用抽象实现接口,在抽象这里整体我们采用一个模板方法的设计模式,通过类型取延时任务的地方是通过反射去获取,表中落得是类的名称,延时任务的 提交 / 获取 / 执行 / 异常处理 / 定时回收中间状态任务机制 都在这个service中实现:
TriggerTaskInfo是数据表映射的实体类 PartitionUtils 是封装的一个多线程异步的工具类,后续会出文章介绍😄🎃 注意这里为什么要开手工事务: 无法使用@Transactional对一个非public的方法进行事务管理 另外: 类似下面这种自调用的事务是不生效的,原因以后的文章会总结
@Service
public class DmzService {
public void saveAB(A a, B b) {
saveA(a);
saveB(b);
}
@Transactional
public void saveA(A a) {
dao.saveA(a);
}
@Transactional
public void saveB(B b){
dao.saveB(a);
}
}
具体实现:
@Slf4j
public abstract class AbstractDelayJobHandler implements DelayJobScheduleService {
@Resource
private TriggerTaskRepository repository;
@Resource(name = "triggerTaskThreadPool")
private ThreadPoolTaskExecutor triggerTaskThreadPool;
@Resource
private TransactionTemplate transactionTemplate;
/**
* 每批大小
*/
private static final int DEFAULT_HANDLE_DELAY_JOB_SIZE = 1;
/**
* 超时时间
*/
private static final int TIMEOUT_SECOND = 120;
@Override
public void handleDelayJob() {
// 查询需要触发的延时任务并将对应的任务更新到中间态
List<TriggerTaskInfo> triggerTasks = queryTriggeredDelayedTasks();
log.info("tasks to be scheduled {}", JacksonUtils.toJsonStr(triggerTasks));
// 业务自定义处理器, 分发任务
distributeTasks(triggerTasks);
// 将任务改为最终态
finish(triggerTasks);
log.info("handleDelayJob success {}", JacksonUtils.toJsonStr(triggerTasks));
}
protected List<TriggerTaskInfo> queryTriggeredDelayedTasks() {
// 这里可以用一些云平台的配置中心动态配置size
return repository.queryTriggeredDelayedTasks(30);
}
protected void distributeTasks(List<TriggerTaskInfo> triggerTasks) {
// 线程池每个线程调度一个任务
PartitionUtils.partitionCall2ListAsync(triggerTasks, DEFAULT_HANDLE_DELAY_JOB_SIZE, TIMEOUT_SECOND, TimeUnit.MINUTES,
triggerTaskThreadPool.getThreadPoolExecutor(), taskInfos -> {
taskInfos.forEach(task -> {
try {
Class clazz = Class.forName(task.getHandler());
AbstractDelayJobHandler delayJobHandler = (AbstractDelayJobHandler)SpringUtil.getBean(clazz);
// 手工事务
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
delayJobHandler.handler(task);
}
});
}catch (Exception e) {
// 失败更新为最初状态
log.error("handleDelayJob fail", e);
repository.init(task);
}
});
return Lists.newArrayList();
});
}
protected abstract void handler(TriggerTaskInfo taskInfo);
protected void finish(List<TriggerTaskInfo> triggerTask) {
// 更新为最终状态
repository.finish(triggerTask);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void handleAbnormalDelayJob() {
// 处理一些因为机器宕机未执行完的任务(更新时间到当前时间较久的)
List<TriggerTaskInfo> triggerTasks = repository.queryAbnormalTasks();
if (CollectionUtils.isEmpty(triggerTasks)) {
return;
}
log.info("begin handleAbnormalDelayJob {}", JacksonUtils.toJsonStr(triggerTasks));
// 将任务改为最初状态
repository.init(triggerTasks);
}
@Override
public void addJob(List<TriggerTaskInfo> triggerTasks) {
repository.addJob(triggerTasks);
log.info("addDelayJob {}", JacksonUtils.toJsonStr(triggerTasks));
}
}
实现具体的一个handler,TriggerTaskInfo为参数类型透传每个handler,具体业务参数可以放到扩展参数json中:
@Component
public class TestDelayJob extends AbstractDelayJobHandler {
@Override
protected void handler(TriggerTaskInfo taskInfo) {
// 业务逻辑
..........
}
}
具体的仓储层实现,这个项目用的是mybatis,这里注意查询符合条件的任务和将任务改为中间状态是两步操作需要上事务。
接口:
public interface DelayJobScheduleService {
/**
* 延迟处理
*/
void handleDelayJob();
/**
* 处理中断的延时任务
*/
void handleAbnormalDelayJob();
/**
* 添加延时任务
*/
void addJob(List<TriggerTaskInfo> triggerTasks);
}
实现:
@Repository
public class TriggerTaskRepositoryImpl implements TriggerTaskRepository {
@Resource
private TriggerTaskMapper triggerTaskMapper;
@Override
public void finish(List<TriggerTaskInfo> triggerTasks) {
triggerTasks.stream().forEach(task -> task.setStatus(TriggerTaskStatusEnum.FINISH.getCode()));
if (CollectionUtils.isEmpty(triggerTasks)) {
return;
}
triggerTaskMapper.batchUpdateByPrimaryKeySelective(BeanCopierUtils.copyList(triggerTasks, TriggerTask.class));
}
@Override
public void init(List<TriggerTaskInfo> triggerTasks) {
triggerTasks.stream().forEach(task -> task.setStatus(TriggerTaskStatusEnum.INIT.getCode()));
triggerTaskMapper.batchUpdateByPrimaryKeySelective(BeanCopierUtils.copyList(triggerTasks, TriggerTask.class));
}
@Override
public void init(TriggerTaskInfo triggerTask) {
triggerTask.setStatus(TriggerTaskStatusEnum.INIT.getCode());
triggerTaskMapper.updateByPrimaryKey(BeanCopierUtils.copyBean(triggerTask, TriggerTask.class));
}
@Override
public List<TriggerTaskInfo> queryAbnormalTasks() {
// 查询触发任务
Example example = new Example(TriggerTask.class);
example.createCriteria()
.andEqualTo("status", TriggerTaskStatusEnum.PROCESS.getCode())
.andLessThan("updateTime", DateUtil.getDateByOffsetHours(new Date(),
CommonConstants.NEGATIVE_ONE));
return BeanCopierUtils.copyList(triggerTaskMapper.selectByExample(example), TriggerTaskInfo.class);
}
@Override
@Transactional(rollbackFor = Exception.class)
public List<TriggerTaskInfo> queryTriggeredDelayedTasks(Integer batchSize) {
// 查询触发任务
Date now = new Date();
Example example = new Example(TriggerTask.class);
example.createCriteria()
.andEqualTo("status", TriggerTaskStatusEnum.INIT.getCode())
.andEqualTo("type", TriggerTaskTypeEnum.DELAY_JOB.getCode())
.andLessThan("triggerTime", now);
RowBounds rowBounds = new RowBounds(CommonConstants.ZERO, batchSize);
List<TriggerTask> triggerTasks = triggerTaskMapper.selectByExampleAndRowBounds(example, rowBounds);
// 更新为中间状态
triggerTasks.stream().forEach(task -> {
task.setUpdateTime(now);
task.setStatus(TriggerTaskStatusEnum.PROCESS.getCode());
});
if (CollectionUtils.isEmpty(triggerTasks)) {
return Lists.newArrayList();
}
triggerTaskMapper.batchUpdateByPrimaryKeySelective(triggerTasks);
return BeanCopierUtils.copyList(triggerTasks, TriggerTaskInfo.class);
}
@Override
public void addJob(List<TriggerTaskInfo> triggerTasks) {
if (CollectionUtils.isEmpty(triggerTasks)) {
return;
}
triggerTaskMapper.insertList(BeanCopierUtils.copyList(triggerTasks, TriggerTask.class));
}
}
最后封装一个口子给到调度服务就大功告成啦:
@Slf4j
@RestController
@RequestMapping("/delayJob")
public class DelayJobScheduleController {
@Autowired
private DelayJobScheduleService delayJobScheduleService;
@ApiOperation(value = "查询并处理延时任务")
@GetMapping(value = "/handleDelayJob")
public void handleDelayJob() {
delayJobScheduleService.handleDelayJob();
}
@ApiOperation(value = "处理异常中止的延时任务")
@GetMapping(value = "/handleAbnormalDelayJob")
public void handleAbnormalDelayJob() {
delayJobScheduleService.handleAbnormalDelayJob();
}
}