什么是工作流? – 工作流解决了什么问题
工作流,是把业务之间的各个步骤以及规则进行抽象和概括性的描述。使用特定的语言为业务流程建模,让其运行在计算机上,并让计算机进行计算和推动。
工作流是复杂版本的状态机。
上图为工作流退化为基础状态机的例子,小明的状态非常简单,站立->走路->跑步->走路->站立,无限循环,如果让我们实现小明的状态切换,那么我们只需要用一个字段来记录小明当前的状态就好了。
而对于复杂的状态或者状态维度增加且状态流转的条件极为复杂,可能单纯用字段记录状态的实现方式就会不那么理想。
如上图所示,现在交给小明的选择就多了起来,当小明获得金钱的时候,他会根据金钱数额的大小来判断接下来该如何行动,如果数额小于等于3000,那么他决定买一个新手机就够了,如果数额小于等于30万,那么小明就决定去学习一下理财,好好利用这笔钱,但如果小明获得的金钱数量超过了30万,他就决定购置一套房屋,但购置房屋的流程是复杂的,小明决定同时完成交首付和贷款的手续。
其实这个流程还不算特别复杂,但到目前为止,单纯用一个字段来表明状态已经无法满足要求了。
工作流解决的痛点在于,解除业务宏观流程和微观逻辑的耦合,让熟悉宏观业务流程的人去制定整套流转逻辑,而让专业的人只需要关心他们应当关心的流程节点,就好比大家要一起修建一座超级体育场,路人甲只需要关心他身边的这一堆砖是怎么堆砌而非整座建筑。
那么工作流有什么不能解决的问题呢?
工作流是一个固定好的框架,大家就按照这个框架来执行流程就行了,但某些情况他本身没有流转顺序的要求,比如:小明每天需要做作业,做运动以及玩游戏,它们之间没有关联性无法建立流程,但可以根据每一项完成的状态决定今天的任务是否完结,这种情况我们需要使用CMMN来建模,它就是专门针对这种情况而设计的,但今天我们不讲这个,而是讲讲BPMN协议。
BPMN2.0协议
对于业务建模,我们需要一种通用的语言来描绘,这样在沟通上和实现上会降低难度,就像中文、英文一样,BPMN2.0便是一种国际通用的建模语言,他能让自然人轻松阅读,更能被计算机所解析。
协议中元素的主要分类为,事件-任务-连线-网关。
一个流程必须包含一个事件(如:开始事件)和至少一个结束(事件)。其中网关的作用是流程流转逻辑的控制。任务则分很多类型,他们各司其职,所有节点均由连线联系起来。
下面我就以每种类型的节点简单地概括一下其作用。
网关:
互斥网关(Exclusive Gateway),又称排他网关,他有且仅有一个有效出口,可以理解为if……else if…… else if……else,就和我们平时写代码的一样。
并行网关(Parallel Gateway),他的所有出口都会被执行,可以理解为开多线程同时执行多个任务。
包容性网关(Inclusive Gateway),只要满足条件的出口都会执行,可以理解为 if(……) do, if (……) do, if (……) do,所有的条件判断都是同级别的。
任务:
BPMN2.0协议的所有任务其实是从一个抽象任务派生而来的,抽象任务会有如下行为:
- 当流程流转到该任务时,应该做些什么?
- 当该任务获得信号(signal)的时候,它是否可以继续向下流转,而任务获得信号的这个动作我们称为Trigger。
利用如上的抽象行为,我们来解释一些比较常见且具有代表性的任务类型。
人工任务(User Task),它是使用得做多的一种任务类型,他自带有一些人工任务的变量,例如签收人(Assignee),签收人就代表该任务交由谁处理,我们也可以通过某个特定或一系列特定的签收人来查找待办任务。利用上面的行为解释便是,当到达User Task节点的时候,节点设置Assignee变量或等待设置Assignee变量,当任务被完成的时候,我们使用Trigger来要求流程引擎退出该任务,继续流转。
服务任务(Service Task),该任务会在到达的时候执行一段自动的逻辑并自动流转。从“到达自动执行一段逻辑”这里我们就可以发现,服务任务的想象空间就可以非常大,我们可以执行一段计算,执行发送邮件,执行RPC调用,而使用最广泛的则为HTTP调用,因为HTTP是使用最广泛的协议之一,它可以解决大部分第三方调用问题,在我们的使用中,HTTP服务任务也被我们单独剥离出来作为一个特殊任务节点。
接受任务(Receive Task),该任务的名字让人费解,但它又是最简单的一种任务,当该任务到达的时候,它不做任何逻辑,而是被动地等待Trigger,它的适用场景往往是一些不明确的阻塞,比如:一个复杂的计算需要等待很多条件,这些条件是需要人为来判断是否可以执行,而不是直接执行,这个时候,工作人员如果判断可以继续了,那么就Trigger一下使其流转。
调用活动(Call Activity),调用活动可以理解为函数调用,它会引用另外一个流程使之作为子流程运行,调用活动跟函数调用的功能一样,使流程模块化,增加复用的可能性。
上面大概介绍了一下常用的节点,下面的图就展示了一个以BPMN2.0为基础的流程模型,尽量覆盖到所介绍的所有节点。
这里是一个生产汽车的流程,从“汽车设计”节点到“批准生产”节点是一个串行的任务,而审批的结果会遇到一个互斥网关,上面讲过,互斥网关只需要满足其中一个条件就会流转,而这里表达的意义就是审批是否通过。“载入图纸”是一个服务任务,它是自动执行的,之后会卡在“等待原材料”这个节点,因为这个节点是需要人为去判断(比如原材料涨价,原材料不足等因素),所以需要在一种自定义的条件下Trigger,而该图的条件应该为“原材料足够”,原材料足够之后,我们会开始并行生产汽车零件。
需要注意的是,并行网关在图中是成对出现的,他的作用是开始一系列并行任务和等待并行任务一起完成,拿一个Java中的东西举例子,就是CountDownLatch,fork-join模型也可以类比。
说到这里,网关的底层逻辑我作为拓展提一句,没听懂也无伤大雅。网关的本质其实是计数器和出口逻辑的混合,它跟其他节点没什么区别,只是他的推动逻辑需要使他的计数器为0,而计数器的总数为网关入口线段的数量,比如“组装”节点前面的并行网关,他的计数器就为4,而前面4个节点,每完成一个就会触发该网关计数器-1。
当计数器为0的时候,网关会触发选择后续流转的逻辑。
互斥网关的逻辑为:遍历所有出口连线,满足条件则流出并打断(也就是break掉)。
并行网关的逻辑为:遍历所有出口连线,无条件为所有连线流出。
包容性网关的逻辑为:遍历所有出口连线,满足条件的就流出。
Flowable以及与其他工作流引擎的对比
Flowable是BPMN2.0协议的一种Java版本的实现。
Flowable项目提供了一组核心的开源业务流程引擎,这些引擎紧凑且高效。它们为开发人员、系统管理员和业务用户提供了一个工作流和业务流程管理(BPM)平台。它的核心是一个非常快速且经过测试的动态BPMN流程引擎。它基于Apache2.0开源协议,有稳定且经过认证的社区。
Flowable可以嵌入Java应用程序中运行,也可以作为服务器、集群运行,更可以提供云服务。
与大多数故事一样,Flowable也是因为其与Activiti对未来规划的路线不认同而开辟了一条自己的道路。目前主流的工作流开源框架就是Activiti/Camunda/Flowable,它们都有一个共同的祖先jbpm。先是有了jbpm4,随后出来了一个Activiti5,Activiti5经过一段时间的发展,核心人员出现分歧,又分出来了一个Camunda。activiti5发展了4年左右,紧接着就出现了Flowable。
相比于Activiti,Flowable的核心思想更像是在做一个多彩的工具,它在工作流的基础功能上,提供了很多其他的扩展,使用者可以随心所欲地把Flowable打造成自己想要的样子。例如:Camel节点,Mule节点。他不仅有bpmn引擎,还有cmmn(案例管理模型),dmn(决策模型),content(内容管理),form(表单管理),idm(用户鉴权)等等,但这也间接导致了Flowable的包结构非常繁多,上手非常困难。
而Activiti则只着重于处理bpmn,它的方向在于云,他的设计会尽量像例如Spring Cloud、Docker、K8S靠拢。
如果你单纯地想快速上手bpmn引擎,建议使用Activiti,如果你想做出花样繁多的工作流引擎,建议使用Flowable。
而Camunda(卡蒙达)则更加的轻巧灵活,他的初衷就是为开发人员设计的“小工具”,但我个人的感觉而言,Camunda从代码上看并没有Activiti和Flowable好,而且他的社区是最不活跃的一个(至少从国内的角度来看),所以不太建议使用(当然这带了很多个人主观感受,如有不同意见,欢迎讨论)。
Flowable的核心架构和一些改进
上图是我个人所理解的架构,从架构图中可以看出,Flowable对于数据的处理是冷热分离的,热数据存在ACT_RU_系列表中,历史冷数据存在ACT_HI_系列表中,定义相关的存在ACT_RE_系列表中,而对于在途和定义相关的数据,有一层缓存,他缓存的具体实现比较复杂,这里不多赘述。
对于协议到运行态的转化,有专门一层Converter来实现,也就是说,如果你想自己定义一些协议外的东西,就需要关心这个部分。
最下面是节点行为层,这个很好理解,对于每种节点,都会有不同的实现,你也可以自定义实现。
Flowable在最新的版本中,对于历史归档和异步任务做了新的优化,具体的看下面。
Flowable开发了新的异步执行器(ASYNC EXECUTOR)
在工作流中,异步执行大概分为两类,timer和message,类似于定时事件就是timer,而异步的服务任务则为message,如上图所示,“Task A”附着的边界定时事件,在时间触发之后,会执行“Escalate”任务,而“Async Service Task”在“Task A”流转之后,会启用一个异步任务去调用其服务。
对于一种全是异步服务任务的极端情况,如上图所示,他常常出现于服务编排之类的场景之中,我们经常性的需要同时处理一系列的任务,这时候异步调度的作用就非常重要。
为了提高性能,Flowable也采用了冷热数据分离的思想,他把数据分为了4类,异步Job,定时器Timer,挂起任务,死信队列。通过测试发现,数据库是异步任务性能低下的主要瓶颈,特别是多实例竞争Job会存在潜在的问题。在分表的时候同时加上了一个全局锁,保证了同一个实例只能由一个实例去获取并锁定job(job表中有字段会被update,内容为抢占到的实例代号),这样反而能提升不少性能。为了保证各个实例不被饿死,还调整了一系列参数。
上图是在100万异步任务(100万流程同时推动)同时并发的情况下的测试数据,很明显,添加更多实例可以提高吞吐量和可伸缩性,这正是引入一个全局锁所带来的效果。同样有趣的是,更改每次Job的获取量也可以显著提高吞吐量。在以前的老版本或Activiti中来说,提高实例反而会降低性能。
PS:对于这部分的优化,我翻译了官方的四篇文章,课后我会发出来,有兴趣的同学可以去看一下。
流程归档异步化
Flowable提供了一个更加优化的冷热数据分离方案,在数据敏感性比较高的领域,我们一般会把引擎的历史记录级别调到最高(包括流程流转历史、变量变动历史、签收人变动历史等等),这些历史记录在以前是在同一个上下文中执行的,虽然在最开始设计的时候,在途数据和历史数据就冷热分离了,但从权衡在途和历史的重要性的角度来讲,历史其实不是最重要的,所以Flowable提供一系列的方法使历史记录这个行为异步化,与之对应的方法可以是jms,rabbitMQ或Spring Boot listener application。
这个改动可以提升在途流程效率20%-96%。
上图是一个简单的流程图,从start节点直接到end节点的性能提升。
上图是全顺序执行的服务任务流程性能测试结果。
上图是多变量的流程图性能测试结果。
PS:对于这部分的优化,其实不参考官方实现,自己也可以做,只要处理好历史查询的延迟容忍度和历史查询ACT_HI_PROCINST流程实例表分裂的问题就可以,因为该表是在途和历史数据共存的,比较特殊。
云上的工作流应该关心什么
首先,从用户的角度来看,使用者其实只需要关心三件事。
- 我如何把我的业务逻辑转化为流程图-即容易理解的绘图工具。
- 我如何使流程流转-即开箱即用的API。
- 我需要引擎告诉我,我现在该处理什么节点-即丰富且鲜明的事件机制。
图中是流程图的整个生命周期,从画图到部署,然后启动流程,流程经过人工或自动的方式流转,最后结束。
对于生命周期中画图这一块,我们必须要剔除一些不常用且费解的节点类型,比如:异常边界事件、信号或消息事件、泳道。还有一些费解的属性,比如,排他、同步异步、相对的,我们只保留一些常见的节点类型,就如前面介绍的:用户任务、服务任务、互斥网关、并行网关等。
而开箱即用的API就需要我们尽量减少API的复杂度和个数,把API分类为以下三类比较合适。
- 流程定义类-负责流程定义的查看
- 流程实例类-负责流程实例的查看与操作
- 任务实例类-负责任务实例的查看与操作
而对于“我现在该处理什么节点”的问题,我们提供了一种解决方案。
用户的角色可以分为三类逻辑,第一、和工作流沟通的逻辑,它负责启动流程和通知流程的流转,第二类为服务提供者,即工作流不能提供的服务,需要第三方或业务方自己计算结果,比如:支付接口。第三类为消息处理逻辑,这里的消息大概为任务的创建,任务的签收,任务的完成,流程的创建,流程的结束等等,处理消息的角色可以根据自己的职责处理不同的任务类型或流程类型。
这样区分的好处在于,如果有一个流程图,他的流程涉及到不同系统甚至是不同部门之间的合作,我们不可能让所有部门都去关心整个流程,甚至有些部门根本不知道工作流的存在,他们所关心的是“我需要做什么,我给出什么结果就OK了”。