通用流程基础框架设计

背景

在如应用打包和广告投放等场景都有明显的流程分多步骤处理过程,在开发时除了要考虑业务逻辑外,还需要考虑其他业务无关的通用功能,比如编排,异常处理,重试,监听回调等等,耗费较多精力和成本。

目标

建设通用流程基础框架,提供编排、重试、监听回调等基础能力,这样开发时只需要关心业务逻辑即可,有效提高开发效率。

框架特点

  1. 基于责任链模式把流程内的多个步骤串联起来,通过流程上下文传递信息。
  2. 拦截器能力,在流程或步骤前后执行,可以实现统一校验或统一异常处理等通用功能。
  3. 不同的流程之间通过转换器串联起来,实现流程间的松解耦。
  4. 通过配置线程池,同类流程允许并行执行,提高效率。
  5. 可以设置步骤级别异步重试策略。
  6. 支持流程的超时重试,一般用于服务重启后流程自动恢复。
  7. 步骤可以注册监听策略,使步骤拥有异步监听的能力,常用于那些异步创建,需通过不断反查来确定最终创建状态的场景。
  8. 与 Spring 框架相结合,方便利用 Spring 框架的强大特性,例如依赖注入等等。

方案设计

类&接口设计

  1. BaseStep 为步骤基类,继承该类类封装步骤逻辑。里面提供基础能力,可以控制流程执行。
  2. Flow 类主要用于装配步骤和连接下一流程。
  3. FlowContext 为流程上下文基类,里面包含流程的基本信息,通过该类的实例决定执行哪一流程。
  4. FlowKeyGenerator 接口,流程 key 生成器,每个流程实例都有一个全局唯一的 key。
  5. FlowFactory 接口,流程工厂,主要用于生产流程,定义运行流程的线程池以及应用的流程名。
  6. FlowManager 接口,流程管理器,用于注册流程工厂,流程拦截器等等,还是流程调用的入口。
  7. FlowExecutor 接口,流程执行器,控制流程执行的核心
  8. FlowInterceptor 接口,在流程执行前后提供埋点。
  9. StepInterceptor 接口,在步骤执行前后提供埋点。
  10. FlowContextConverter 接口,FlowContext 转换器,流程结束后通过转换器连接下一流程。

流程拦截器调用顺序

流程拦截器提供三个埋点,分别是:

  1. 流程提交到线程池前(beforeSubmit):由于流程从提交到线程池到被线程池调度有一定的时延,所以特地提供此埋点。
  2. 流程执行前(beforeRun)
  3. 流程执行完后(afterRun):该埋点会有一个 Exception 类型的参数,接收步骤或 StepInterceptor 或 FlowInterceptor#beforeRun方法产生的异常

具体调用过程如下图:

流程转换器

流程转换器相当于一个中转站,把不同流程连接起来,支持分裂(一对多)。

加上流程转换器后的流程执行模型就变成这样了:

异步重试机制

调研了 Spring-retry 与 Guava-retry 开源框架发现都是同步阻塞重试,浪费线程资源,影响性能。

于是重新设计了一个异步重试模块,默认实现基于时间轮+线程池,

支持多种延时策略:

  • 固定延时
  • 随机时间延时
  • 递增式延时
  • 指数级递增延时

和多种重试判断器:

  • 最大重试次数判断
  • 特定异常判断
  • 混合判读
  • 自定义判断

并且支持注解的方式配置。

异步监听步骤

考虑一个常见的场景:一个步骤申请了某个请求,请求需要审核比较耗时,通常需要不断重试监听请求的审核状态,直到审核通过或者失败,此时步骤才算完成。

流程框架也提供这种步骤的能力。前面提到的重试监听通常需要到另外的线程中处理,此时流程会暂停往下进行,直到重试结束,再根据监听结果来自行决定流程是否继续。

处理模型大致如下:

流程执行超时重试

常用于服务器重启后自动恢复流程。

设计方案分为两大模块:

第一个模块:记录流程上下文和过期时间

  1. 可以在流程处设置默认的步骤重试策略,也可以在步骤处配置特定的超时重试策略。
  2. 利用流程拦截器和步骤拦截器,在流程提交时和步骤执行前,把当前流程上下文保存到 Redis hash 类型key里,以及把步骤的过期时间存到 Redis 的有序队列里。
  3. 流程结束时删除 Redis 里的流程上下文和有序队列里的记录。

第二个模块,轮询监听过期流程

  1. 超时监听器轮询 Redis 有序队列,获取第一条记录(第一条记录代表最先过期的流程)

  2. 判断这条记录是否已经过期

    1. 如果未过期,忽略,睡眠等待下一次轮询
    2. 如果已过期,从有序队列删除记录,获取流程上下文并提交给流程管理器,该流程将从超时的步骤开始重新执行
  3. 开始下一次轮询

答疑

  1. 疑问:超时的流程上下文可能会被多个实例同时发现,如何保证只有一个实例拿到这个流程的重试权?

答: 发现超时流程上下文时,对该流程上下文从有序队列里删除,利用 Redis 单线程执行命令的特性,只会有一个实例删除成功,删除成功的实例才会去重试流程。


  1. 疑问:假如步骤仍在运行,但是由于运行时间过长超过了超时时间,导致超时监听器认为该流程超时并且重新分发该流程,此时会不会出现同一流程(flowKey 一样)多处运行的情况?如何解决的?

答: 这种场景下,确实会出现同一流程多处运行,但仅限当前步骤,从下一步骤开始只会单处运行。框架里尽最大程度避免出现这种问题:

  1. 步骤拦截器会在步骤执行前:更新当前流程上下文的更新时间。步骤执行后:比较当前流程上下文的更新时间与远端(Redis)保存的流程上下文的更新时间,如果当前流程的较小,说明当前流程已经过时,直接抛出异常 结束流程;
  2. 步骤内每次重试都会更新流程上下文的更新时间,避免重试太多太久让监听器误以为流程已经超时。

需要注意的点

  1. 因为有重试的存在,所以要求每个步骤的逻辑必须保证幂等。
  2. 因为整个流程可能不在同一线程里执行,所以可以不使用 ThreadLocal 就不要使用 ThreadLocal,可以使用 FlowContext 来代替。
  3. 如果某些场景只能使用 ThreadLocal,也有开源方案可以解决,例如下面这个: github.com/alibaba/tra...
相关推荐
凡人的AI工具箱12 分钟前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang
java亮小白199720 分钟前
Spring循环依赖如何解决的?
java·后端·spring
2301_8112743137 分钟前
大数据基于Spring Boot的化妆品推荐系统的设计与实现
大数据·spring boot·后端
草莓base1 小时前
【手写一个spring】spring源码的简单实现--容器启动
java·后端·spring
Ljw...2 小时前
表的增删改查(MySQL)
数据库·后端·mysql·表的增删查改
编程重生之路2 小时前
Springboot启动异常 错误: 找不到或无法加载主类 xxx.Application异常
java·spring boot·后端
薯条不要番茄酱2 小时前
数据结构-8.Java. 七大排序算法(中篇)
java·开发语言·数据结构·后端·算法·排序算法·intellij-idea
qq_174482857510 小时前
springboot基于微信小程序的旧衣回收系统的设计与实现
spring boot·后端·微信小程序
锅包肉的九珍10 小时前
Scala的Array数组
开发语言·后端·scala
心仪悦悦10 小时前
Scala的Array(2)
开发语言·后端·scala