延迟重试任务实现方案记录

目标:实现延迟重试任务

注意事项:

  1. 尽量兼容各种需延迟重试场景。如:业务返回可重试错误码,网络异常,中间件异常等。
  2. 重试逻辑异常或失败,不再创建新的任务。
  3. 有完整记录,可追溯失败任务。
  4. 尽量支持多种延迟策略

设计方案:

字段名字段说明
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方案缺点:依赖中间件,可能存在调度任务丢失问题
执行逻辑缺点:如果业务逻辑与重试逻辑一致,需重复造轮子。优点:可以兼容所有业务情况,考虑后续拓展多种任务类型,如消息队列重放任务,远程接口重试任务等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值