通用流程基础框架设计

背景

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

目标

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

框架特点

  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...
相关推荐
齐 飞28 分钟前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod44 分钟前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。1 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man2 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*2 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu2 小时前
Go语言结构体、方法与接口
开发语言·后端·golang
cookies_s_s2 小时前
Golang--协程和管道
开发语言·后端·golang
为什么这亚子2 小时前
九、Go语言快速入门之map
运维·开发语言·后端·算法·云原生·golang·云计算
想进大厂的小王2 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
customer083 小时前
【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源·intellij-idea