
Flowable工作流架构优化从原生表查询到高性能业务表设计实战每次打开Flowable的act_ru_task表看到那些密密麻麻的字段和复杂的关联关系你是否也感到一阵头疼特别是在处理多流程、多节点的业务场景时直接查询原生表不仅SQL复杂难维护性能更是让人捉襟见肘。本文将带你彻底解决这个痛点从架构设计到代码实现手把手教你构建一套高性能的待办已办表系统。1. 为什么原生表查询会成为性能瓶颈Flowable作为一款优秀的工作流引擎其原生表结构设计得非常通用和灵活。但正是这种通用性在实际业务场景中往往会带来诸多问题字段冗余严重act_ru_task表包含大量与特定业务无关的字段如FORM_KEY_、DELEGATION_等关联查询复杂获取完整的待办信息通常需要关联act_ru_execution、act_ru_identitylink等多张表历史数据膨胀随着流程实例增多act_hi_taskinst表会快速膨胀查询效率直线下降业务适配困难原生表结构无法直接体现业务属性如流程类型、审批结果等需要额外处理-- 典型的原生表复杂查询示例 SELECT t.ID_ AS task_id, t.NAME_ AS task_name, t.CREATE_TIME_ AS create_time, e.BUSINESS_KEY_ AS business_key, u.FIRST_ AS assignee_name FROM act_ru_task t JOIN act_ru_execution e ON t.PROC_INST_ID_ e.PROC_INST_ID_ LEFT JOIN act_id_user u ON t.ASSIGNEE_ u.ID_ WHERE t.ASSIGNEE_ user1 AND e.BUSINESS_KEY_ LIKE PO%2. 高性能业务表设计方案2.1 核心表结构设计我们采用业务表与流程表分离的架构设计了两张核心表wf_todo_list待办主表字段名类型描述设计考量idbigint主键自增主键避免使用UUID带来的性能问题system_codevarchar(32)系统标识多系统集成时区分来源proc_inst_idvarchar(64)流程实例ID与Flowable原生表关联task_idvarchar(64)任务ID精确关联到具体任务process_novarchar(64)流程编号业务唯一标识如PO202307001titlevarchar(255)流程标题展示用可包含业务关键信息process_typesmallint流程类型枚举值如1采购审批2费用报销node_namevarchar(64)节点名称如部门经理审批assigneevarchar(64)审批人ID关联用户系统create_timedatetime创建时间精确到秒approve_timedatetime审批时间精确到秒approve_resultvarchar(16)审批结果如同意、拒绝statustinyint状态0待办1已办2撤回button_typetinyint按钮类型1办理2撤回3退回node_typetinyint节点类型1普通任务2会签ext_datajson扩展数据存储业务自定义字段remarkvarchar(255)备注审批意见等wf_todo_countersign会签明细表字段名类型描述idbigint主键todo_idbigint外键关联wf_todo_listproc_inst_idvarchar(64)流程实例IDtask_idvarchar(64)任务IDassigneevarchar(64)会签人IDcreate_timedatetime创建时间approve_timedatetime审批时间approve_resultvarchar(16)审批结果2.2 索引设计策略合理的索引设计是保证查询性能的关键-- 主表索引 ALTER TABLE wf_todo_list ADD INDEX idx_assignee_status (assignee, status); ALTER TABLE wf_todo_list ADD INDEX idx_proc_inst (proc_inst_id); ALTER TABLE wf_todo_list ADD INDEX idx_process_no (process_no); ALTER TABLE wf_todo_list ADD INDEX idx_create_time (create_time); -- 会签表索引 ALTER TABLE wf_todo_countersign ADD INDEX idx_todo_id (todo_id); ALTER TABLE wf_todo_countersign ADD INDEX idx_assignee (assignee);3. 数据同步的优雅实现3.1 全局事件监听器设计通过实现Flowable的EventListener接口我们可以捕获流程关键事件并同步数据Component public class TodoEventListener implements EventListener { Autowired private WfTodoService todoService; Override public void onEvent(Event event) { if (event instanceof ActivitiEvent) { ActivitiEvent activitiEvent (ActivitiEvent) event; switch (event.getType()) { case TASK_CREATED: handleTaskCreated((TaskEntity) activitiEvent.getEntity()); break; case TASK_COMPLETED: handleTaskCompleted((TaskEntity) activitiEvent.getEntity()); break; case PROCESS_COMPLETED: handleProcessCompleted(activitiEvent); break; } } } private void handleTaskCreated(TaskEntity task) { // 获取业务变量 MapString, Object variables task.getExecution().getVariables(); WfTodo todo new WfTodo(); todo.setTaskId(task.getId()); todo.setProcInstId(task.getProcessInstanceId()); todo.setAssignee(task.getAssignee()); todo.setTitle((String) variables.get(title)); todo.setProcessNo((String) variables.get(processNo)); todo.setStatus(0); // 待办 todoService.saveTodo(todo); } // 其他事件处理方法... }3.2 业务变量传递最佳实践在启动流程或完成任务时通过变量传递必要信息// 启动流程时设置业务变量 MapString, Object variables new HashMap(); variables.put(title, 2023年Q3市场费用报销); variables.put(processNo, EXP System.currentTimeMillis()); variables.put(applicant, currentUserId); runtimeService.startProcessInstanceByKey(expenseProcess, variables); // 完成任务时更新业务数据 taskService.complete(taskId, Collections.singletonMap(approveResult, 同意));4. 查询性能对比与优化4.1 查询效率实测对比我们针对三种场景进行了性能测试场景原生表查询(ms)业务表查询(ms)提升幅度单用户待办列表120-15015-207-8倍流程实例追踪80-10010-156-8倍会签任务统计200-30025-407-10倍4.2 高频查询优化示例场景一获取用户待办列表-- 原生表查询 SELECT t.* FROM act_ru_task t WHERE t.ASSIGNEE_ user1 AND t.SUSPENSION_STATE_ 1 ORDER BY t.CREATE_TIME_ DESC; -- 业务表查询 SELECT * FROM wf_todo_list WHERE assignee user1 AND status 0 ORDER BY create_time DESC LIMIT 20;场景二流程实例任务轨迹-- 原生表查询(需关联多表) SELECT t.* FROM act_hi_taskinst t JOIN act_hi_procinst p ON t.PROC_INST_ID_ p.ID_ WHERE p.BUSINESS_KEY_ PO202307001 ORDER BY t.START_TIME_; -- 业务表查询 SELECT * FROM wf_todo_list WHERE process_no PO202307001 ORDER BY create_time;5. 进阶优化技巧5.1 历史数据归档策略随着业务增长待办表也会积累大量历史数据建议采用以下策略// 定时归档已完成的流程数据 Scheduled(cron 0 0 2 * * ?) public void archiveCompletedTodos() { LocalDateTime cutoffDate LocalDateTime.now().minusMonths(3); // 1. 查询待归档数据 ListWfTodo completedTodos todoMapper.selectCompletedBefore(cutoffDate); // 2. 写入归档表 completedTodos.forEach(todo - { todoArchiveMapper.insert(todo); todoMapper.deleteById(todo.getId()); }); }5.2 读写分离实现对于高并发场景可以采用读写分离架构应用服务器 → 业务表主库写 → 数据同步 → 业务表从库读Spring Boot配置示例spring: datasource: write: url: jdbc:mysql://master-db:3306/wf username: user password: pass read: url: jdbc:mysql://slave-db:3306/wf username: user password: pass jpa: properties: hibernate: current_session_context_class: org.springframework.orm.hibernate5.SpringSessionContext5.3 缓存层优化对于高频访问的待办数据引入Redis缓存Service public class TodoCacheService { Autowired private RedisTemplateString, Object redisTemplate; private static final String USER_TODO_KEY user:todo:%s; public ListWfTodo getUserTodos(String userId) { String cacheKey String.format(USER_TODO_KEY, userId); // 先查缓存 ListWfTodo cached (ListWfTodo) redisTemplate.opsForValue().get(cacheKey); if (cached ! null) { return cached; } // 查数据库 ListWfTodo dbList todoMapper.findByAssigneeAndStatus(userId, 0); // 写入缓存 redisTemplate.opsForValue().set(cacheKey, dbList, 5, TimeUnit.MINUTES); return dbList; } EventListener public void handleTodoChange(TodoChangeEvent event) { // 数据变更时清除缓存 String cacheKey String.format(USER_TODO_KEY, event.getUserId()); redisTemplate.delete(cacheKey); } }