目标:实现延迟重试任务
注意事项:
- 尽量兼容各种需延迟重试场景。如:业务返回可重试错误码,网络异常,中间件异常等。
- 重试逻辑异常或失败,不再创建新的任务。
- 有完整记录,可追溯失败任务。
- 尽量支持多种延迟策略
设计方案:
字段名 | 字段说明 |
---|---|
id | 唯一ID |
namespace | 系统标识 |
beanName | 标记重试类或重试方法的,可自定义,如method |
retryType | 重试类型:定时重试,指数重试(2^重试次数) |
retryIntervalTime | 重试间隔时间(单位:分钟) |
retryTimes | 重试最大次数 |
currRetryTimes | 当前已重试次数 |
nextRetryTime | 下次重试时间 |
data | 重试数据 |
taskStatus | 任务状态:等待执行,执行成功,执行失败,执行中 |
createTime | 创建时间 |
(一) MongoDB + SaturnJob
简述:
1.新增重试任务至任务表
2.定时任务查询到时间执行的任务,修改任务状态至执行中(分布式锁加锁,定时任务参数可指定只执行某系统任务,提高执行效率)
3.兜底任务,将下次重试时间为当前时间的5分钟前,任务状态为执行中的任务,重置为待执行。
核心代码块:
//定义执行类
public interface IRetryService {
public boolean retry(String retryData);
}
public class Test implements IRetryService{
public void doSomething(Object obj) {
logger.info("正常业务逻辑 begining");
//
boolean result = false;
try{
result = something(obj);
}catch(Exception e){
log.error("异常原因:", e)
}
if(!result){
delayRetryService.addTask("执行类名称",重试次数:3)
}
logger.info("正常业务逻辑 ending");
}
@Override
public boolean retry(String retryData) {
logger.info("开始重试逻辑");
boolean result = doRetry(retryData);
logger.info("重试结果:" + result);
return result;
}
}
//查找对应的任务执行类,或下发至对应系统接口或消息队列,仅本系统
//简要说明:下发至接口则直接获取执行结果返回, 下发至消息队列的话则异步返回执行结果,若无结果返回视为失败,利用兜底任务将任务状态重置
public class SaturnJob {
//加分布式锁 namespace + beanName
List<DelayRetryTask> tasks = delayRetryService.list(namespace, beanName);
delayRetryService.updataStatusToProcessing(tasks);
//释放锁
for(DelayRetryTask task : tasks){
//本地执行逻辑,或下发至消息队列,后续逻辑异步执行。
IRetryService retryService = (IRetryService )SpringContextHolder.getBean(task.getBeanName())
boolean result = false;
try{
result = retryService.retry(task.getData())
} catch(Exception e){
log.error("异常原因:", e)
}
//更新任务详情,增加重试次数,计算下一次重试时间,result为false时,判断是否到达最大重试次数,是:状态为执行失败 否:状态为待执行
flushTaskInfo(task);
delayRetryService.updateTask(task);
}
}
(二) redis (或 + db)
简述:
1.利用Redission的延迟队列RDelayedQueue来实现任务调度
2.若任务量不是很大,可以选择分布式缓存存储;但若是想留存任务详情或任务量大时,使用db存储。
核心代码块:
RBlockingQueue<String> queue = redissonClient.getBlockingQueue(name);
RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(queue);
//循环取出队列元素
while (true) {
if (redissonClient.isShutdown()) {
LOGGER.warn("redis连接异常,定时任务终止");
return;
}
String taskId = null;
try {
//队列take()和poll()的区别:
// take():返回队列的头元素,并把它从队列中删除,如果队列为空时则阻塞线程直到有新的元素添加进来并返回;
// poll():返回队列的头元素,并把它从队列中删除,如果队列头元素为空则返回null但是不阻塞线程;
taskId = queue.take();
LOGGER.info("队列名称:{},取出后队列长度:{},taskId:{}", queue.getName(), queue.size(), taskId);
//处理任务逻辑同上,查询任务详情,处理任务,更新任务详情,重新放入队列中
runTask(taskId);
} catch (Exception e) {
if (!redissonClient.isShutdown()) {
LOGGER.error("定时任务执行失败", e);
}
} finally {
//重新计算触发时间
String cron = stringOps.get(getKey(taskId));
if (null != cron) {
long nextTriggerTime = getNextTriggerTime(cron);
if (nextTriggerTime > 0) {
delayedQueue.offer(taskId, nextTriggerTime, TimeUnit.MILLISECONDS);
}
}
}
}
方案优缺点:
SaturnJob方案缺点:使用分布式锁,执行性能有上线,定时任务不能准确时间重试,存在误差
Redis方案缺点:依赖中间件,可能存在调度任务丢失问题
执行逻辑缺点:如果业务逻辑与重试逻辑一致,需重复造轮子。优点:可以兼容所有业务情况,考虑后续拓展多种任务类型,如消息队列重放任务,远程接口重试任务等。