Flowable工作流别再直接查act表了!手把手教你设计一张高性能待办已办表
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(待办主表)
| 字段名 | 类型 | 描述 | 设计考量 |
|---|---|---|---|
| id | bigint | 主键 | 自增主键,避免使用UUID带来的性能问题 |
| system_code | varchar(32) | 系统标识 | 多系统集成时区分来源 |
| proc_inst_id | varchar(64) | 流程实例ID | 与Flowable原生表关联 |
| task_id | varchar(64) | 任务ID | 精确关联到具体任务 |
| process_no | varchar(64) | 流程编号 | 业务唯一标识,如"PO202307001" |
| title | varchar(255) | 流程标题 | 展示用,可包含业务关键信息 |
| process_type | smallint | 流程类型 | 枚举值,如1=采购审批,2=费用报销 |
| node_name | varchar(64) | 节点名称 | 如"部门经理审批" |
| assignee | varchar(64) | 审批人ID | 关联用户系统 |
| create_time | datetime | 创建时间 | 精确到秒 |
| approve_time | datetime | 审批时间 | 精确到秒 |
| approve_result | varchar(16) | 审批结果 | 如"同意"、"拒绝" |
| status | tinyint | 状态 | 0=待办,1=已办,2=撤回 |
| button_type | tinyint | 按钮类型 | 1=办理,2=撤回,3=退回 |
| node_type | tinyint | 节点类型 | 1=普通任务,2=会签 |
| ext_data | json | 扩展数据 | 存储业务自定义字段 |
| remark | varchar(255) | 备注 | 审批意见等 |
wf_todo_countersign(会签明细表)
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | bigint | 主键 |
| todo_id | bigint | 外键关联wf_todo_list |
| proc_inst_id | varchar(64) | 流程实例ID |
| task_id | varchar(64) | 任务ID |
| assignee | varchar(64) | 会签人ID |
| create_time | datetime | 创建时间 |
| approve_time | datetime | 审批时间 |
| approve_result | varchar(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) { // 获取业务变量 Map<String, 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 业务变量传递最佳实践
在启动流程或完成任务时,通过变量传递必要信息:
// 启动流程时设置业务变量 Map<String, 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-150 | 15-20 | 7-8倍 |
| 流程实例追踪 | 80-100 | 10-15 | 6-8倍 |
| 会签任务统计 | 200-300 | 25-40 | 7-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. 查询待归档数据 List<WfTodo> 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 RedisTemplate<String, Object> redisTemplate; private static final String USER_TODO_KEY = "user:todo:%s"; public List<WfTodo> getUserTodos(String userId) { String cacheKey = String.format(USER_TODO_KEY, userId); // 先查缓存 List<WfTodo> cached = (List<WfTodo>) redisTemplate.opsForValue().get(cacheKey); if (cached != null) { return cached; } // 查数据库 List<WfTodo> 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); } }