企业级复杂业务工作流引擎(Activiti)深度定制与高并发性能优化实战

举报
yd_216727992 发表于 2026/10/31 10:26:28 2026/10/31
【摘要】 企业级复杂业务工作流引擎(Activiti)深度定制与高并发性能优化实战摘要:在大型企业级系统中(如 ERP、OA、供应链金融及复杂审批流),业务流程往往伴随着动态路由、多级会签、任意节点驳回以及高并发审批等复杂诉求。直接裸用开源工作流引擎(如 Activiti / Flowable)的默认配置,不仅难以支撑中国式复杂审批场景,在历史数据膨胀至千万级后更会遭遇严重的性能雪崩。本文将从底层核...

企业级复杂业务工作流引擎(Activiti)深度定制与高并发性能优化实战

摘要:在大型企业级系统中(如 ERP、OA、供应链金融及复杂审批流),业务流程往往伴随着动态路由、多级会签、任意节点驳回以及高并发审批等复杂诉求。直接裸用开源工作流引擎(如 Activiti / Flowable)的默认配置,不仅难以支撑中国式复杂审批场景,在历史数据膨胀至千万级后更会遭遇严重的性能雪崩。本文将从底层核心表结构切入,结合完整生产级源码,深度剖析 Activiti 的自定义节点跳转(任意退回)、多实例动态会签、以及高并发场景下的引擎性能调优实战。


一、 为什么默认的工作流引擎扛不住“中国式复杂审批”?

在标准的 BPMN 2.0 规范中,流程的流转是严格遵循有向图连线(Sequence Flow)进行的。然而在国内真实的政企与大型集团业务场景中,产品经理往往会提出如下硬核需求:

  1. 任意节点驳回(自由跳转):审批人处于第 5 级节点时,发现第 2 级节点填写的资料有误,要求直接将流程退回至第 2 级节点,且第 2 级修改重新提交后,能够选择直接回到第 5 级或重新走完第 3、4 级。
  2. 动态会签与加签/减签:一个采购合同需要多个部门负责人同时审批,支持“一票否决”、“按比例通过(如超过 70% 同意即流转)”,甚至在审批中途动态拉入法务专员进行“临时加签”。
  3. 千万级历史数据下的查询雪崩:随着系统运行一两年,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:

  1. 获取当前正在执行的任务实体(TaskEntity)及其对应的执行实例(ExecutionEntity)。
  2. 从 BpmnModel 内存中读取目标节点(Target FlowElement)的定义信息。
  3. 清理当前任务与关联子执行实例,直接将当前 ExecutionEntity 的当前活动节点指针(CurrentFlowElement)强行修改为目标节点。
  4. 借助 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 老年代内存!
  • 黄金法则:
    1. 只存路由控制变量:只把网关(ExclusiveGateway)分支判断必须用到的极少数字段(如 amount、deptId、approveOutcome)存入引擎。
    2. 业务数据外置关联:所有业务详情一律保存在业务自己的独立表(如 t_purchase_order)中,仅通过 BUSINESS_KEY_(业务主键) 与流程实例进行一对一绑定。

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 索引中,实现毫秒级多维检索。

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 处理企业级审批流时,遇到过最奇葩的业务流转需求是什么?欢迎在评论区一起交流探讨!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。