一个简单的业务重试方案[EventBus+Executor+AOP]

11 篇文章 0 订阅
4 篇文章 0 订阅

对于系统中出现的异常,有的需要告警出来,譬如参数校验不通过,写操作因为幂等的原因失败;有的则需要进行业务重试,譬如 rpc 调用超时。如何设计一个优良的重试机制呢?个人认为应当具备下面几点。

  1. 侵入性小:实现重试的代码逻辑与现有的业务逻辑应尽可能地分离,所谓不侵入,少耦合,重试逻辑与正常逻辑解耦。
  2. 动态配置:重试的最大次数、重试的间隔时间、是否采用线程池进行重试、是否异步重试等,可在项目正常运行期间动态配置而无需项目重启。
  3. 通用性强:适用于绝大多数需要重试的业务场景。

言归正传,如何实现一个优良的重试机制呢?这里我采用的是注解+切面的方式。在需要进行重试的方法上加上一个用以重试的自定义注解,在切面中实现具体的重试逻辑。整体的设计如下:

一.设计一个注解 @Retry,其中定义了两个字段 bizNobizType,后续根据不同的业务类型实现不同的业务逻辑。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retry {

/**
 * 业务单号
 * @return
 */
String businessNo();

/**
 * 业务类型
 * @return
 */
TradeBizType businessType();
}

二. 设计一个切面,用以正常的业务代码执行的时候,横向添加重试逻辑。

@Aspect
@Component
public class RetryAspect {

/**
 * @param point
 * @param reTry
 * @return
 * @throws Throwable
 */
@Around("(@annotation(reTry))")
public Object around(ProceedingJoinPoint point, Retry reTry) throws Throwable {
    Object result = null;
    try{
        //1.业务逻辑正常执行
        result = point.proceed();
        //2.返回 true 时,表明重试成功,需删除异常表对应的数据。
        if(result){
			deleteRetry(point, reTry);
		}
        
    } catch (Exception e){
        //3.出现异常则保存异常条目进异常表或更新原有异常表数据(reTryTimes、reTryVersion、gmt_modified)
        saveOrUpdateRetry(point, reTry, e);
    }
    return result;
}
}

三. 也许细心的小可爱已经发现了,在第二步,result 为 true-成功时,我怎么知道是重试任务的成功执行还是初次任务的成功执行呢?仅有当 result 为重试任务的成功执行结果时,我才需要去删除对应的重试记录条目。此时,可以借助当前线程的线程名称来判断。具体实现如下:

        //...
        result = point.proceed();
        if(Thread.currentThread().getName().startsWith("BIZ_RETRY")){
			deleteBizRetry(point, bizRetry);
		}
        //...

**四. 你也许会问了,线程名称的默认名称不是类似于 Thread-0的么?在哪儿自定义的以 BIZ_RETRY 开头呢?忘了告诉你,具体怎么调用重试任务了。这里我采用的是 EventBus+Executor 线程池。实现将大批量的重试任务异步并多线程并发执行。首先定义一个EventBusUtil 工具类,里头定义了执行重试任务的线程池,异步处理的 EventBus 以及监听器 Listener。 **

public class EventBusUtils {

//1.在这我们设置了处理重试任务的线程所特有的线程名前缀
private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(4, 8, 10000,
	TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100000), new ThreadFactoryBuilder()
	.setNameFormat("BIZ_RETRY-%d").build());

private static AsyncEventBus eventBus = new AsyncEventBus(EventBusUtils.class.getSimpleName(), EXECUTOR);
private static EventListener listener = new EventListener();

static {
    //2.监听器的注册以及预启动所有的核心线程
	eventBus.register(listener);

	EXECUTOR.prestartAllCoreThreads();
}

/**
 * @param bean 
 * @param methodName  
 * @param args  
 */
public static void invoke(Object bean, String methodName, Object... args) {
	Event event = new Event(bean, methodName, args);
	eventBus.post(event);
}
}

五.有了上面处理重试任务的工具类,那么具体应该怎么调用呢?一般情况下,我们会通过定时任务按照一定的间隔时间触发重试,先从重试表中获取符合条件的重试任务,比如重试次数小于等于3次的,重试次数大于3次还没成功的,姑且认为它朽木不可雕也。然后调用 EventBusUtils 的静态方法 invoke 进行重试。到这还没结束呢,invoke 方法的参数应该如何填写呢?

六.上面抛出的问题就涉及到了重试表应该怎么设计,包含哪些必要的字段,我们知道 EventBus 是基于反射的,要想依托它来调用特定的业务重试逻辑,必然需要知道执行什么类的什么方法,参数列表是什么。所以我们的重试表中应该含有 Calss 类型的 目标类,String 类型的 methodName,还有 MethodParam[] params,这三个属性可以封装成一个类,在 saveOrUpdateRetry() 方法中将相应的信息填入重试表中。p.s:第一步其中 MethodParam 是我们自定义的类。大致的代码示例如下:

public class RetryContext {

/**
 * 目标类
 */
private Class targetClass;

/**
 * 方法名称
 */
private String methodName;

/**
 * 方法参数
 */
private MethodParam[] methodParams;

static class MethodParam {

    /**
     * 参数类型
     */
    private Class type;

    /**
     * 值
     */
    private String value;
}}

七.看到这,大家应该清楚了,核心的实现 invoke 的逻辑:

//...省略从重试表获取 context 的过程
RetryEventBusUtils.invoke(ApplicationContextUtil.getBean(context.getTargetClass()), context.getMethodName(), context.getMethodArgs());

end

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值