步骤编排与执行

hogwarts_legacy-wallpaper-2880x1800_看图王.jpg

拼命寻找的、想得到,到最后却往往最容易失去

在上篇文章中我与DDD第一次邂逅提到了DomainStep,DomainStep作为领域服务的更细粒度的存在,将业务活动进行拆解,抽象成一些业务步骤。由于业务的隔离,在顶层设计中应给予Step足够的包容性适应开发人员实现的步骤。

提供注解@Step:

public @interface Step {

    /**
     * 对应{@link Service} value
     */
    @AliasFor(annotation = Service.class, attribute = "value") String value() default "";

    /**
     * 该步骤的名称.
     */
    String name() default "";

    /**
     * 该步骤依赖哪些其他步骤.
     * 即,被依赖的步骤先执行,才能执行本步骤
     */
    Class<? extends DomainStep>[] replyOn() default {};
}
复制代码

在Step中replyOn放置DomainStep class,以这个为依据对Step进行编排。

编排

课堂小知识:拓扑排序。将有向图中的顶点以线性方式进行排序,是指对于任何连接自顶点u到顶点v的有向边uv,在最后的排序结果中,顶点u总是出现在顶点v的前面。

image.png

例如,图的顶点可能代表将要被执行的任务,边代表一个任务必须在另一个任务之前执行。在该应用场景中,一个拓扑排序结果就是一个有效的任务序列。

一个有向图能进行拓扑排序的充要条件是,它是一个有向无环图(Directed Acyclic Graph)。

课堂小知识:有向无环图(DAG)。在一个图中,如果一个有向图无法从某个顶点出发经过若干条边回到该点,则这个图是一个有向无环图。

任意DAG至少有一个拓扑排序结果,并且已知算法可以在线性时间内为任意DAG产生一个拓扑排序结果。

在关于拓扑排序的问题上存在一个线性时间解。若有向图中存在n个结点,则我们可以在O(n)时间内得到其拓扑排序,或在O(n)时间内确定该图不是有向无环图,也就是说对应的拓扑排序不存在。
image.png

我们可获得拓扑排序结果:[1,2,5,3,6,4]、[1,2,5,3,6,4]

以上做个简单的了解,在顶层设计中StepExecute执行Step步骤应可以串行或并行执行。步骤的编排要解决的是排序出步骤执行顺序与以分层形式的可并行步骤。
image.png

  • 思路

    • 将所有入度的值放置在数组中,遍历数组寻找入度为0的元素,即同一遍历得出入度为0的Step为同一层次.一次执行直到所有Step都提取出来。
  • 伪代码

    // inDegree 为入度数量对应Step的数组
    private List<StepLayer> topologicalSort() {
        while (已检出的Step个数<=总个数>) {
            // 初始化
            List<Step> list = new ArrayList();
            StepLayer stepLayer = new StepLayer();
            stepLayer.setSteps(new ArrayList<>());

            for (int i = 0; i < inDegree.length; i++) {
                if (判断入度数) {
                    检出个数++;
                    list.add(step)
                }
            }

            for (String s : list) {
                StepLayer.add(s)
            }
            result.add(stepLayer);
        }
        return result;
    }
复制代码

在进行Step的编排后一定要释放不需要的内存,Step的编排只需要一次(果然是渣男),保存最终结果即可。
同样,除了动态编排Step也可以直接静态输入Step的编排顺序,当然要做好开发人员之间的文档沟通。

执行

在顶层设计中提供了抽象类StepExecute来执行Step,在执行Step的过程出现异常时暂时采用补偿机制,即Step的子类RevokableDomainStep存在方法rollback()。

/**
 * 支持回滚的 activity step
 */
public interface RevokableDomainStep<Model extends DomainModel, 
                                     Ex extends RuntimeException> extends IDomainStep<Model, Ex> {

    /**
     * 执行本步骤的回滚操作,进行操作矫正.
     *
     * 尽可能的处理好影响,Sagas模式并不能严格保证一致性
     *
     * @param model 领域模型
     * @param cause {@link IDomainStep#execute(IDomainModel)}执行过程中抛出的异常,即回滚原因
     */
    void rollback(@NotNull Model model, @NotNull Ex cause);
}
复制代码

image.png

以层级作为执行单元,即为其异步执行(在真实的开发中大部分都是一个Step一层),为了在日志中能显性输出并行的Step的日志需要借助MDC来作为线程的标识,让开发人员能快速定位。

private void asyncExecuteStep(SchedulingTaskExecutor taskExecutor, Step step, Model model) {
        Map<String, String> mdcContext = MDC.getCopyOfContextMap();
        taskExecutor.execute(() -> {
            MDC.setContextMap(mdcContext);
            try {
                step.execute(model);
            } finally {
                MDC.clear();
            }
        });
    }
复制代码

在执行Step的过程中最重要的自然时异常的处理,这个关系到Step是否能正常结束。执行过程中会碰到哪些异常:线程池 full、不需要回滚的业务异常、强制回滚的异常等

// 异常类型的判定,是否是需要执行回滚的异常
private Class resolveStepExType() {
        // 判定这个类是否是动态代理目标类
        Class thisClass=获取动态代理目标类;
        ResolvableType stepsExecType = ResolvableType.forClass(thisClass);
        ResolvableType templateType = stepsExecType.getSuperType();
        
        // StepExecute也会跟随业务的多态而改变,从其基类开始寻找
        while (templateType.getGenerics().length == 0) {
            templateType = templateType.getSuperType();
        }

        // 找到了Step的泛型定义,然后找Step的Ex泛型的具体类型
        ResolvableType stepType = templateType.getGeneric(0);

        // Step实现多个接口的场景
        for (ResolvableType stepInterfaceType : stepType.getInterfaces()) {
            if (DomainStep.class.isAssignableFrom(stepInterfaceType.resolve())) {
                return stepInterfaceType.getGeneric(1).resolve();
            }
        }
        
        ...

        // should never happen
        log.error("Cannot tell Step.Ex type for {}", this.getClass());
        return null;
    }
复制代码

总结

上述使用拓扑排序、Saga机制、栈基本实现了Step的执行,随着业务的复杂度增加可能这一套需要淘汰更新。对于现在的需求,没有强一致性的、时间敏感度高等要求大大为我降低了门槛。

探索的道路需要不断的修改,技术为业务服务,在学习的同时也需要实践。每次使用曾经学习的东西完成新的东西都有一种开悟的感觉,引用 邓宁-克鲁格的心理效应来说,在我们这个年纪其实就是开悟之坡。