Flowable 待办已办查询:从原生表到定制化表的架构演进与实践

📅 2026/6/30 11:17:13
Flowable 待办已办查询:从原生表到定制化表的架构演进与实践
1. 为什么我们需要重新思考Flowable待办已办查询方案第一次接触Flowable工作流引擎时我最头疼的就是待办已办查询这个功能。记得当时直接用了原生表查询方案结果在项目上线三个月后就遇到了大麻烦。当时系统接入了七个业务流程每个流程的查询条件都不一样SQL语句写了几百行后来每次修改需求都像在拆炸弹。原生表方案最大的问题在于act_ru_task和act_hi_task这两张表的设计是为了记录流程运行状态而不是为了优化查询效率。当你的系统只有一两个简单流程时可能感受不明显但随着业务复杂度提升以下几个痛点会越来越明显SQL复杂度爆炸不同流程需要关联不同的业务表where条件层层嵌套性能瓶颈历史任务表数据量大了之后分页查询变得异常缓慢维护困难每次新增流程都要修改查询逻辑牵一发而动全身扩展性差想加个自定义排序或者特殊过滤条件都很麻烦我见过最夸张的一个案例某企业的采购审批系统里有15个流程查询SQL写了1200多行执行时间超过8秒。后来他们改用定制化表方案重构后查询响应时间直接降到200毫秒以内。2. 两种技术方案的深度对比2.1 原生表查询方案解析直接查询act_ru_task和act_hi_task是最简单的入门方案新手开发者很容易想到这种方式。它的优势在于零开发成本直接使用Flowable现有表结构不需要考虑数据同步问题适合快速验证原型或小型项目但它的局限性在复杂场景下会暴露无遗。比如需要查询某个采购流程的待办时你可能需要这样写SQLSELECT t.* FROM act_ru_task t JOIN act_ru_execution e ON t.PROC_INST_ID_ e.PROC_INST_ID_ JOIN biz_purchase_order o ON e.BUSINESS_KEY_ o.id WHERE t.ASSIGNEE_ user1 AND o.department IT AND o.amount 10000这还只是一个简单场景如果遇到会签、子流程等复杂情况SQL会变得难以维护。2.2 定制化中间表方案详解定制化表方案的核心思想是用空间换时间用冗余换清晰。我们设计专门的wf_todo_list表来存储待办已办数据它的优势非常明显查询简单只需单表查询或简单关联性能优化可以针对查询场景设计索引业务解耦查询逻辑与流程引擎解耦扩展灵活可以自由添加业务字段我最近实施的一个项目中使用定制化表后查询性能提升了40倍。关键点在于表结构设计时考虑了这些因素流程实例信息proc_inst_id、process_no等任务基本信息task_id、node_name等业务上下文title、process_type等审批状态approve_result、done_type等扩展能力extjson字段存储动态数据3. 定制化表的设计艺术3.1 主表wf_todo_list的精妙设计经过多个项目的迭代我总结出一套高效的待办表设计方案。以文中提到的wf_todo_list为例有几个设计亮点值得注意会签处理用node_type2标记会签节点避免重复存储流程追踪last_todo_id字段形成审批链业务分离process_no既包含业务类型又包含唯一ID状态集中done_type统一管理待办/已办状态特别要提的是extjson字段的设计。在实际项目中不同流程往往需要携带不同的业务数据。比如请假流程需要显示请假天数报销流程需要显示金额合同审批需要显示对方公司把这些动态数据以JSON格式存储既保持了表结构的稳定又满足了灵活展示的需求。3.2 会签详情表的协同设计wf_todo_countersign表的设计有几个关键点外键关联通过todo_id关联主表精简字段只存储会签特有信息状态独立每个会签人可以独立审批在实际编码中处理会签任务时可以这样操作// 处理会签任务时同步数据 public void handleCountersign(String taskId, String assignee) { // 查询主任务 WfTodo mainTask todoMapper.selectByTaskId(taskId); // 插入会签明细 WfTodoCountersign detail new WfTodoCountersign(); detail.setTodoId(mainTask.getId()); detail.setAssignee(assignee); // 其他字段设置... countersignMapper.insert(detail); }4. 数据同步机制的实现之道4.1 全局监听器的实战应用数据同步是定制化表方案最核心的部分。我推荐使用Flowable的事件监听器机制来实现自动同步。具体实现可以这样做public class TodoEventListener implements TaskListener { Override public void notify(DelegateTask task) { String eventName task.getEventName(); switch(eventName) { case EVENTNAME_CREATE: handleTaskCreate(task); break; case EVENTNAME_COMPLETE: handleTaskComplete(task); break; // 其他事件处理... } } private void handleTaskCreate(DelegateTask task) { // 从流程变量中获取业务数据 String title (String)task.getVariable(title); String processNo (String)task.getVariable(processNo); // 构建待办记录 WfTodo todo new WfTodo(); todo.setTaskId(task.getId()); todo.setTitle(title); todo.setProcessNo(processNo); // 其他字段设置... // 保存到待办表 todoMapper.insert(todo); } }4.2 业务数据传递的技巧在实战中业务数据的传递有几个实用技巧启动流程时通过流程变量传递title、processNo等业务数据任务办理时通过TaskService.setVariable更新数据使用ThreadLocal在业务方法间传递临时数据JSON序列化复杂对象可以序列化后存入extjson我曾经遇到一个场景需要在待办列表显示审批金额区间如1万-5万。解决方案是在流程启动时计算好这个描述存入流程变量// 启动流程时 MapString, Object variables new HashMap(); variables.put(title, 采购审批); variables.put(amountRange, calculateAmountRange(purchaseOrder)); runtimeService.startProcessInstanceByKey(purchase, businessKey, variables);5. 复杂场景下的架构优势5.1 多流程统一查询的优雅实现在电商系统中我们可能有订单审核、退货审批、供应商准入等多个流程。使用定制化表后统一查询变得非常简单SELECT * FROM wf_todo_list WHERE assignee user123 AND done_type 0 ORDER BY create_time DESC如果需要按流程类型过滤只需增加一个条件AND process_type 25.2 高性能分页的解决方案对于数据量大的系统分页查询性能至关重要。定制化表可以这样优化索引设计在assigneedone_typecreate_time上建联合索引延迟关联先查ID再关联获取详细信息游标分页避免传统limit分页的性能问题这里给出一个优化后的分页查询示例-- 第一页查询 SELECT id, title, process_type, create_time FROM wf_todo_list WHERE assignee user1 AND done_type 0 ORDER BY create_time DESC LIMIT 10; -- 后续页查询使用上一页最后一条的create_time SELECT id, title, process_type, create_time FROM wf_todo_list WHERE assignee user1 AND done_type 0 AND create_time 2023-06-01 12:00:00 ORDER BY create_time DESC LIMIT 10;5.3 历史数据归档策略随着系统运行待办表数据会不断增长。我建议采用以下归档策略热冷分离最近3个月数据放在主表历史数据归档到历史表定时任务每月初执行归档操作查询路由根据时间范围自动选择查主表还是历史表实现代码示例// 归档上月数据 Scheduled(cron 0 0 1 1 * ?) public void archiveLastMonthData() { Date archiveDate DateUtils.addMonths(new Date(), -3); ListWfTodo oldTodos todoMapper.selectBeforeDate(archiveDate); if(!oldTodos.isEmpty()) { todoHistoryMapper.batchInsert(oldTodos); todoMapper.deleteBeforeDate(archiveDate); } }6. 踩坑指南与最佳实践在实施定制化表方案的过程中我积累了一些宝贵经验数据一致性保障使用事务保证流程操作和待办表更新的一致性添加补偿机制处理异常情况定期运行数据校验任务并发处理技巧使用乐观锁防止并发更新冲突高频更新字段单独拆分表监控与报警监控待办表同步延迟设置数据量增长预警测试建议模拟高并发场景测试同步性能验证流程回滚时数据一致性一个典型的补偿任务实现public void checkAndFixTodoData() { // 查找可能不同步的数据 ListString taskIds todoMapper.findUnsyncedTasks(); for(String taskId : taskIds) { Task task taskService.createTaskQuery().taskId(taskId).singleResult(); if(task null) { // 任务已完成但待办表未更新 todoMapper.markAsDone(taskId); } else { // 待办表记录丢失重新创建 WfTodo todo buildTodoFromTask(task); todoMapper.insert(todo); } } }在实际项目中这套方案已经帮助我成功应对过20复杂流程的系统。特别是在最近的一个金融风控系统中面对每天10万的任务量定制化表方案依然保持了毫秒级的查询响应。