企业级复杂业务工作流引擎(Activiti)深度定制与高并发性能优化实战
企业级复杂业务工作流引擎(Activiti)深度定制与高并发性能优化实战
摘要:在大型企业级系统中(如 ERP、OA、供应链金融及复杂审批流),业务流程往往伴随着动态路由、多级会签、任意节点驳回以及高并发审批等复杂诉求。直接裸用开源工作流引擎(如 Activiti / Flowable)的默认配置,不仅难以支撑中国式复杂审批场景,在历史数据膨胀至千万级后更会遭遇严重的性能雪崩。本文将从底层核心表结构切入,结合完整生产级源码,深度剖析 Activiti 的自定义节点跳转(任意退回)、多实例动态会签、以及高并发场景下的引擎性能调优实战。
一、 为什么默认的工作流引擎扛不住“中国式复杂审批”?
在标准的 BPMN 2.0 规范中,流程的流转是严格遵循有向图连线(Sequence Flow)进行的。然而在国内真实的政企与大型集团业务场景中,产品经理往往会提出如下硬核需求:
- 任意节点驳回(自由跳转):审批人处于第 5 级节点时,发现第 2 级节点填写的资料有误,要求直接将流程退回至第 2 级节点,且第 2 级修改重新提交后,能够选择直接回到第 5 级或重新走完第 3、4 级。
- 动态会签与加签/减签:一个采购合同需要多个部门负责人同时审批,支持“一票否决”、“按比例通过(如超过 70% 同意即流转)”,甚至在审批中途动态拉入法务专员进行“临时加签”。
- 千万级历史数据下的查询雪崩:随着系统运行一两年,
ACT_HI_*(历史表)数据量突破数千万行,原本几十毫秒的“待办任务查询(Todo List)”或“已办任务列表”接口响应时间飙升至数秒甚至触发数据库死锁。
要彻底解决这些问题,我们不能仅停留在调用 taskService.complete(taskId) 的表层 API,必须深入 Activiti 的命令模式(Command Pattern)底层执行引擎与数据库索引模型。
二、 庖丁解牛:Activiti 核心表结构与运行期/历史期分离机制
在进行深度定制和性能优化之前,必须摸清引擎底层的表分类逻辑。Activiti 的表统一以 ACT_ 开头,核心分为四大类:
| 表分类前缀 | 核心代表表 | 职责说明与并发特征 | 性能瓶颈风险等级 |
|---|---|---|---|
ACT_RE_* (Repository) |
ACT_RE_PROCDEF, ACT_RE_DEPLOYMENT |
流程定义与静态资源表。存储 BPMN XML 解析模型,读多写极少。 | 低(引擎内置部署缓存) |
ACT_RU_* (Runtime) |
ACT_RU_EXECUTION, ACT_RU_TASK, ACT_RU_VARIABLE |
运行时核心表。仅存储正在流转中的未完结流程,流程一旦结束立即清空。高频读写。 | 极高(高并发锁竞争与慢查询重灾区) |
ACT_HI_* (History) |
ACT_HI_PROCINST, ACT_HI_TASKINST, ACT_HI_VARINST |
历史归档表。记录所有已完结和运行中的轨迹,只增不减,数据量极易突破千万级。 | 高(大表全表扫描、分页深翻页卡死) |
ACT_GE_* (General) |
ACT_GE_BYTEARRAY, ACT_GE_PROPERTY |
通用全局数据表。存储大字段二进制数据及全局主键生成策略(NextDbId)。 | 中高(默认主键生成策略存在全局行锁瓶颈) |
核心设计启示:Activiti 之所以将
RU(运行时)和HI(历史)拆开,核心目的就是为了保证ACT_RU_*表始终处于“小表”状态,从而提高流转效率。一旦线上出现流程推进缓慢,90% 的原因都是变量滥用导致ACT_RU_VARIABLE膨胀,或者历史同步写入级别过高拖慢了主事务。
三、 硬核定制实战一:基于 Command 模式的“任意节点自由跳转/驳回”
在 BPMN 图中,如果为了实现“节点 C 退回到节点 A”,去流程图上画满密密麻麻的逆向连线,不仅流程图会变成无法维护的“蜘蛛网”,一旦节点增加更是灾难。
1. 底层实现原理
Activiti 引擎内部基于**职责链 + 命令模式(Command<T>)**实现。无论是启动流程还是完成任务,最终都是封装成一个 Command 交由 CommandExecutor 在同一个事务上下文(CommandContext)中执行。
要实现任意节点跳转,我们只需自定义一个 JumpAnyWhereCmd:
- 获取当前正在执行的任务实体(
TaskEntity)及其对应的执行实例(ExecutionEntity)。 - 从
BpmnModel内存中读取目标节点(Target FlowElement)的定义信息。 - 清理当前任务与关联子执行实例,直接将当前
ExecutionEntity的当前活动节点指针(CurrentFlowElement)强行修改为目标节点。 - 借助
Agenda(引擎执行议程)触发ContinueProcessOperation,让引擎按照新指针继续向下运转!
2. 生产级源码实现(基于 Activiti 6.x / 7.x 架构)
import org.activiti.bpmn.model.BpmnModel;
import org.activiti.bpmn.model.FlowElement;
import org.activiti.engine.ActivitiException;
import org.activiti.engine.impl.context.Context;
import org.activiti.engine.impl.interceptor.Command;
import org.activiti.engine.impl.interceptor.CommandContext;
import org.activiti.engine.impl.persistence.entity.ExecutionEntity;
import org.activiti.engine.impl.persistence.entity.ExecutionEntityManager;
import org.activiti.engine.impl.persistence.entity.TaskEntity;
import org.activiti.engine.impl.persistence.entity.TaskEntityManager;
import org.activiti.engine.impl.util.ProcessDefinitionUtil;
/**
* 生产级自定义命令:实现工作流任意节点自由跳转/驳回
*/
public class JumpToTargetNodeCmd implements Command<Void> {
/** 当前待办任务ID */
private final String currentTaskId;
/** 目标节点定义Key (对应 BPMN XML 中的 userTask id) */
private final String targetNodeId;
/** 驳回/跳转原因备注 */
private final String jumpReason;
public JumpToTargetNodeCmd(String currentTaskId, String targetNodeId, String jumpReason) {
this.currentTaskId = currentTaskId;
this.targetNodeId = targetNodeId;
this.jumpReason = jumpReason;
}
@Override
public Void execute(CommandContext commandContext) {
ExecutionEntityManager executionEntityManager = commandContext.getExecutionEntityManager();
TaskEntityManager taskEntityManager = commandContext.getTaskEntityManager();
// 1. 查询当前任务实体并校验合法性
TaskEntity currentTask = taskEntityManager.findById(currentTaskId);
if (currentTask == null) {
throw new ActivitiException("跳转失败:当前任务不存在或已被处理,taskId=" + currentTaskId);
}
// 2. 获取当前执行实例与流程定义模型
String executionId = currentTask.getExecutionId();
ExecutionEntity currentExecution = executionEntityManager.findById(executionId);
String processDefinitionId = currentExecution.getProcessDefinitionId();
BpmnModel bpmnModel = ProcessDefinitionUtil.getBpmnModel(processDefinitionId);
// 3. 解析目标节点 FlowElement
FlowElement targetFlowElement = bpmnModel.getMainProcess().getFlowElement(targetNodeId);
if (targetFlowElement == null) {
throw new ActivitiException("跳转失败:目标节点在流程模型中不存在,targetNodeId=" + targetNodeId);
}
// 4. 记录审批驳回意见到历史表 (ACT_HI_COMMENT)
commandContext.getCommentEntityManager().addComment(
currentTaskId, currentExecution.getProcessInstanceId(), "JUMP_REJECT", jumpReason
);
// 5. 删除当前运行期任务(标记删除原因为跳转驳回,并同步更新 ACT_HI_TASKINST 结束时间)
taskEntityManager.deleteTask(currentTask, jumpReason, false, true);
// 6. 【核心】篡改当前执行实例的节点指针,指向目标节点
currentExecution.setCurrentFlowElement(targetFlowElement);
// 7. 将执行实例推入引擎 Agenda 议程队列,驱动目标节点重新生成待办任务
Context.getAgenda().planContinueProcessOperation(currentExecution);
return null;
}
}
Service 层优雅调用方式:
@Service
public class WorkflowCustomService {
@Autowired
private ManagementService managementService;
/**
* 将当前任务驳回至任意历史节点
*/
@Transactional(rollbackFor = Exception.class)
public void rejectToNode(String taskId, String targetActivityId, String reason) {
managementService.executeCommand(new JumpToTargetNodeCmd(taskId, targetActivityId, reason));
}
}
四、 硬核定制实战二:多实例动态会签(Multi-Instance)与比例投票控制
在复杂合同评审中,常需要多个部门负责人同时审批。BPMN 2.0 提供了 multiInstanceLoopCharacteristics 标签支持并行(Parallel)或串行(Sequential)多实例会签。
1. BPMN XML 核心配置
在会签节点中,引擎会自动维护三个极其关键的内置局部变量:
nrOfInstances:会签任务的总实例数(总共需要几个人批)。nrOfCompletedInstances:当前已经完成审批的实例数。nrOfActiveInstances:当前尚未完成的实例数。
<userTask id="deptManagerCounterSign" name="多部门负责人联合会签" activiti:assignee="${signer}">
<multiInstanceLoopCharacteristics isSequential="false"
activiti:collection="signerList"
activiti:elementVariable="signer">
<!-- 完成条件表达式:调用自定义 Spring Bean 计算投票结果 -->
<completionCondition>${counterSignVoteBean.isComplete(execution)}</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>
2. 自定义会签投票裁决组件(支持一票否决与权重比例通过)
@Component("counterSignVoteBean")
@Slf4j
public class CounterSignVoteBean implements Serializable {
/**
* 每次有会签人员提交审批时,引擎都会回调该方法判断是否结束整个会签节点
*
* @param execution 当前执行上下文
* @return true: 立即结束会签并流转到下一节点; false: 继续等待其他人审批
*/
public boolean isComplete(DelegateExecution execution) {
// 1. 获取本次审批人传入的投票结果 (APPROVED / REJECTED)
String voteResult = (String) execution.getVariableLocal("voteResult");
// 2. 获取内置会签统计变量
int totalInstances = (Integer) execution.getVariable("nrOfInstances");
int completedInstances = (Integer) execution.getVariable("nrOfCompletedInstances");
// 3. 统计当前累计同意票数
Integer approvedCount = (Integer) execution.getVariable("approvedCount");
if (approvedCount == null) {
approvedCount = 0;
}
if ("APPROVED".equals(voteResult)) {
approvedCount++;
execution.setVariable("approvedCount", approvedCount);
}
// 策略 A:【一票否决制】只要有一人投反对票,立刻终止会签,设置全局路由变量为 REJECT
if ("REJECTED".equals(voteResult)) {
log.info("会签触发一票否决,流程实例ID={}", execution.getProcessInstanceId());
execution.setVariable("globalCounterSignOutcome", "REJECT");
return true; // 返回 true 销毁其余未审批人的待办任务,立刻向下流转
}
// 策略 B:【比例通过制】同意人数达到或超过 70%,直接提前通过,无需等待剩余人员
double passRatio = (double) approvedCount / totalInstances;
if (passRatio >= 0.70) {
log.info("会签同意比例已达标 ({}%), 提前通过会签", passRatio * 100);
execution.setVariable("globalCounterSignOutcome", "PASS");
return true;
}
// 策略 C:所有人都已审批完毕,但同意比例仍未满 70%,判定为会签不通过
if (completedInstances == totalInstances) {
execution.setVariable("globalCounterSignOutcome", "REJECT");
return true;
}
return false;
}
}
五、 生产环境高并发性能调优“五大杀手锏”
当企业流程引擎日均发起量达到数十万单、历史数据突破千万级时,默认的引擎配置必然引发数据库连接池爆满与频繁死锁。以下是经过生产实战检验的五大核心调优手段:
1. 彻底替换默认的全局主键生成器(解决 ACT_GE_PROPERTY 行锁瓶颈)
- 痛点根因:Activiti 默认使用
DbIdGenerator生成所有表的主键。它每次分配 ID 块时,都会去UPDATE ACT_GE_PROPERTY SET VALUE_ = ... WHERE NAME_ = 'next.dbid'并递增REV_乐观锁版本号。在高并发下,大量线程在竞争同一行记录的排他锁或触发乐观锁重试,性能极其低下! - 调优方案:注入基于 UUID 或 分布式雪花算法(Snowflake) 的
IdGenerator,实现零数据库 I/O 的内存级唯一主键生成。
@Configuration
public class ActivitiEngineOptimizationConfig implements ProcessEngineConfigurationConfigurer {
@Override
public void configure(SpringProcessEngineConfiguration configuration) {
// 1. 替换默认数据库表锁ID生成器,改用高性能 StrongUuidGenerator (基于 Time-based UUID) 或自定义雪花算法
configuration.setIdGenerator(new StrongUuidGenerator());
// 2. 调整历史记录级别:若无需审计每个局部变量的瞬时变更,由 FULL 降级为 AUDIT 或 ACTIVITY
configuration.setHistoryLevel(HistoryLevel.AUDIT);
// 3. 扩大流程定义内存缓存上限,防止频繁解析 BPMN XML 与反序列化大对象
configuration.setProcessDefinitionCacheLimit(1024);
// 4. 开启批量 SQL 执行优化 (MyBatis Batch 模式)
configuration.setJdbcBatchProcessing(true);
}
}
2. 严禁滥用流程变量(Process Variables 瘦身原则)
- 反模式警告:很多开发者图省事,直接把整个业务大表单 DTO(包含几十个字段甚至 Base64 字符串)直接通过
runtimeService.startProcessInstanceByKey(key, businessKey, hugeMap)塞进流程变量。 - 致命后果:这会导致
ACT_RU_VARIABLE和ACT_HI_VARINST表极速膨胀至上亿行;更可怕的是,每次流程流转时引擎都会全量加载并反序列化关联变量,瞬间撑爆 JVM 老年代内存! - 黄金法则:
- 只存路由控制变量:只把网关(ExclusiveGateway)分支判断必须用到的极少数字段(如
amount、deptId、approveOutcome)存入引擎。 - 业务数据外置关联:所有业务详情一律保存在业务自己的独立表(如
t_purchase_order)中,仅通过BUSINESS_KEY_(业务主键) 与流程实例进行一对一绑定。
- 只存路由控制变量:只把网关(ExclusiveGateway)分支判断必须用到的极少数字段(如
3. 告别 TaskQuery 连表慢查询:构建“业务+待办宽表”或同步 Elasticsearch
- 痛点:业务页面通常要求按照“采购单号模糊搜索 + 申请人部门 + 申请金额区间 + 当前待办人”进行复合分页查询。如果使用引擎自带的
taskService.createTaskQuery().processVariableValueEquals(...),底层会生成极其恐怖的多重INNER JOIN ACT_RU_VARIABLE嵌套 SQL,在十万级在途流程下直接变成秒级慢 SQL。 - 破局架构:
- 中小型规模(百万级以内):直接用 MyBatis 编写自定义原生 SQL,利用
ACT_RU_TASK直接JOIN业务主表(通过BUSINESS_KEY_关联),避开ACT_RU_VARIABLE表。 - 大型高并发规模(千万级以上):利用 Activiti 的 全局事件监听器(
ActivitiEventListener) 监听TASK_CREATED、TASK_ASSIGNED、TASK_COMPLETED事件,将待办任务与业务冗余字段异步投递至 RocketMQ 并写入一张专门的**“企业统一待办中心宽表”**或 Elasticsearch 索引中,实现毫秒级多维检索。
- 中小型规模(百万级以内):直接用 MyBatis 编写自定义原生 SQL,利用
4. 核心生产级索引补齐清单(DBA 必看)
开源版本的建表脚本中,部分高频检索字段默认缺少复合索引,上线前务必检查并补齐以下索引:
-- 1. 加速通过业务主键 (BusinessKey) 关联查询流程实例
CREATE INDEX idx_ru_exec_buskey ON ACT_RU_EXECUTION(BUSINESS_KEY_);
CREATE INDEX idx_hi_proc_buskey ON ACT_HI_PROCINST(BUSINESS_KEY_);
-- 2. 加速个人待办任务列表查询 (按办理人 + 创建时间倒序分页)
CREATE INDEX idx_ru_task_assignee_create ON ACT_RU_TASK(ASSIGNEE_, CREATE_TIME_ DESC);
-- 3. 加速候选组/角色待办查询
CREATE INDEX idx_ru_idlink_group_type ON ACT_RU_IDENTITYLINK(GROUP_ID_, TYPE_);
-- 4. 加速个人已办历史任务列表查询
CREATE INDEX idx_hi_task_assignee_end ON ACT_HI_TASKINST(ASSIGNEE_, END_TIME_ DESC);
5. 历史归档表(ACT_HI_*)冷热分离策略
对于已经完结(END_TIME_ IS NOT NULL)超过 6 个月的历史流程数据,切勿让其长期驻留在主库:
- 编写分布式定时任务(如 XXL-Job),在每日凌晨低峰期,按
PROC_INST_ID_批次将已完结超过半年的ACT_HI_PROCINST、ACT_HI_TASKINST、ACT_HI_ACTINST、ACT_HI_VARINST数据迁移至归档历史库(Archive DB)或 ClickHouse / TiDB 中,并在主库执行小批次DELETE,确保主库历史表水位常年保持在数百万行以内。
六、 总结
工作流引擎绝不仅仅是一个简单的“状态流转机”。在企业级架构落地中,只有深入理解其内部的 Command 命令拦截器机制 与 Execution 执行树模型,才能优雅地搞定中国式复杂审批中的“自由跳转”与“动态会签”;同时,谨守**“业务数据与引擎变量彻底解耦、替换数据库锁 ID 生成器、构建独立待办检索宽表”**三大性能铁律,才能让工作流引擎在千万级数据体量下依然保持丝滑流转。
你在接入 Activiti 或 Flowable 处理企业级审批流时,遇到过最奇葩的业务流转需求是什么?欢迎在评论区一起交流探讨!
- 点赞
- 收藏
- 关注作者
评论(0)