Skip to main content

补偿恢复的代码设计思路

· 9 min read

最近在做平台推送相关的业务接口,而因为对这个推送一开始就提出了明确的要求,当出现错误的时候,能够进行补偿,也是基于这一点,想做一种通用的补偿方案。

在此基础上,我提出我的一点小小的需求,我希望在出错时,进行补偿,只需要从出错的代码开始重新执行即可,此前的代码不需要在执行,避免个别地方没有保证幂等性的操作

//1.保存a表

//2.保存b表 发生错误

//3.执行推送

在步骤2发生错误时,比如网络超时,那么在我自动重试补偿时,我希望从步骤2开始执行,而非从头来过,这样的好处就是 如果a表没有一些处理可能会造成重复插入的问题

首先我的想法就是设计一个动作行为类,他能够保存每个动作(步骤),内部有一个序列号 比如这样

Action action = new Action();
action.next(saveA())
.next(saveB())
.next(saveC())
.executor();

next()指定下一个动作,内部用链表结构去存储,通过executor()去有序执行

下一步我们该考虑异常该如何做

我们能有什么统一的异常兜底方案吗?其实也有,只是不一定能满足所有人,前面我说了,我希望设计一个通用的方案,所以最好的方式就是把选择权交给用户,然后我再提供一个实践方案供大家参考,如果觉得不合适,可以考虑自己写一套实践方案,这样做,灵活性会大大提高

    public boolean executor() {

boolean runState = true;
for (OperationAction operationAction : operationActions) {
if (runState) {
try {

runState = operationAction.run();
} catch (Exception e) {
runState = false;

handleAsyncFallback(operationAction);

logger.error(e);

}
} else {
break;
}
}

return runState;
}

executor()内部进行了异常捕获,当发生异常时,进行handleAsyncFallback 执行

    private void handleAsyncFallback(OperationAction operationAction) {
new Thread(() -> handler.apply((T) operationAction.param, operationAction)).start();
}

通过handler去执行触发,handler是一个函数式接口

private final Action<T, OperationAction> handler;

@FunctionalInterface
public interface Action<T,E> {
void apply(T t,E e);
}

用起来也很简单,在创建动作类时,构造函数进行初始化即可,效果如下

        OperationAction<ActionLogRecord> operationAction = new OperationAction<>((record, action) -> {
//失败策略
record.setStepOrder(action.getOrder());
record.setInnerId(DBUtil.makeID());
record.setUserId(UserContext.getUserId());
record.setCreateUser(UserContext.getUserName());
record.setAdditionalInfo3(JSON.toJSONString(materialPusherReq));
logService.saveActionLogRecord(record);
});

我的方案是通过,当发生异常时,我保存在异常信息表中,方便后面扫描使用

后面就是跳跃执行的方法,其实这个就比较简单了,我们在发生异常时都已经保存了步骤,也就是执行到哪个动作,内部for循环时去跳过之前的步骤就可以了

    /**
* 跳过指定步骤执行
*
* @param index 从第几步开始执行
* @return
*/
public boolean skipExecutor(int index) {
return skipExecutor(index, true);
}

/**
* 跳过步骤继续执行
*
* @return
*/
public boolean skipExecutor(int index, boolean handleExceptions) {

if (index > 1) {
for (int i = 0; i < index - 1; i++) {
operationActions.removeFirst();
}
}


boolean runState = true;
for (OperationAction operationAction : operationActions) {
if (runState) {
try {
runState = operationAction.run();
} catch (Exception e) {

runState = false;
if (handleExceptions) {
handleAsyncFallback(operationAction);
}
logger.error(e);

}
} else {
break;
}
}

return runState;
}

这样,就完成了步骤的跳跃,当然在实际开发上,还有一些问题

比如说,我约束了每个步骤的返回结果,都必须为true或者false

当步骤2需要依赖步骤1的处理结果时,这一点就显得不那么友好,我这里的方案是建议大家用上下文去解决

在我以为一切都比较完美的时候,我又迎来了另一个需求,我的每个动作步骤的名称是啥啊,我希望保存到异常信息表的时候,能够给予异常信息表一些内容,比如每个步骤特殊的参数,需要记录起来,方便事后回滚使用,再比如说步骤的名称,如果没有名称,表中只有1,2,3,4步 看不到每个步骤代表的含义,如果我能执行步骤之前,传递给失败策略参数,那么就太好了,所以我定义了一个withParam

    public OperationAction<T> withParam(T param) {
this.param = param;
return this;
}

最后让我们看一下用起来的效果

完整操作动作类附上


import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Function;

/**
* 操作动作类
*/
public class OperationAction<T> {


private static final Logger logger = Logger.getLogger(OperationAction.class);


private final AtomicInteger count;

private final Action<T, OperationAction> handler;

private T param;

private int order;

private Function<T, Boolean> action;

private LinkedList<OperationAction> operationActions = new LinkedList<>();


public OperationAction(Action<T, OperationAction> operation) {
count = new AtomicInteger(0);
handler = operation;
}

private OperationAction(int order, Action<T, OperationAction> handler, T param, Function<T, Boolean> action) {
this.order = order;
this.handler = handler;
this.param = param;
this.count = null;
this.action = action;
}


public OperationAction<T> next(Function<T, Boolean> action) {
return this.next(this.param, action);
}


public OperationAction<T> next(T param, Function<T, Boolean> action) {
operationActions.add(new OperationAction(count.incrementAndGet(), handler, param, action));
return this;
}

public boolean executor() {

boolean runState = true;
for (OperationAction operationAction : operationActions) {
if (runState) {
try {

runState = operationAction.run();
} catch (Exception e) {
runState = false;

handleAsyncFallback(operationAction);

logger.error(e);

}
} else {
break;
}
}

return runState;
}

private void handleAsyncFallback(OperationAction operationAction) {
new Thread(() -> handler.apply((T) operationAction.param, operationAction)).start();
}

/**
* 跳过指定步骤执行
*
* @param index 从第几步开始执行
* @return
*/
public boolean skipExecutor(int index) {
return skipExecutor(index, true);
}

/**
* 跳过步骤继续执行
*
* @return
*/
public boolean skipExecutor(int index, boolean handleExceptions) {

if (index > 1) {
for (int i = 0; i < index - 1; i++) {
operationActions.removeFirst();
}
}


boolean runState = true;
for (OperationAction operationAction : operationActions) {
if (runState) {
try {
runState = operationAction.run();
} catch (Exception e) {

runState = false;
if (handleExceptions) {
handleAsyncFallback(operationAction);
}
logger.error(e);

}
} else {
break;
}
}

return runState;
}


public boolean run() {
Boolean flag = this.action.apply(this.param);
if (!flag) {
this.handler.apply(this.param, this);
}
return flag;
}


public int getOrder() {
return order;
}


public OperationAction<T> withParam(T param) {
this.param = param;
return this;
}


}


因为已经过去两个多月了,现在才抽空把这个写成一篇文章供大家参考,现在有一些新的想法,可以升级改造一下,针对param,其实可以考虑使用aop,在每个方法上加上,这样写法上,就更加优雅了,如果能够明确动作顺序,甚至可以再想个办法,通过注解+一步调用的方法,就能实现整个动作类的执行,不过本文仅提供一个最基础版本的思路

真正发生异常了以后,通过定时器+ 跳跃执行,就能够完成步骤恢复重试

📌「你的身份来自你的习惯,每个行动都是你在投票给你想成为的人。要想使自己做到最好,你需要持续编辑你的信念,升级和扩展你的身份 」 — 掌控习惯