1. 高性能待办列表的架构挑战在SaaS平台中构建待办列表功能时我们常常会遇到几个典型痛点。最近接手的一个多租户项目里当并发用户数超过500时系统响应时间从原来的200ms飙升到2秒以上这让我不得不重新思考待办列表的设计方案。核心瓶颈往往出现在三个方面首先是数据库查询效率当用户任务量达到万级时简单的LIMIT分页会导致深度翻页性能骤降其次是权限过滤的复杂度特别是候选人、候选组这类动态权限场景最后是状态管理的混乱比如任务在待办、拾取、归还等状态间的流转缺乏统一管控。我见过最糟糕的实现是把所有任务数据加载到内存中处理。某次事故排查发现一个租户管理员查询待办列表时后端竟然加载了全公司3万多条任务记录进行内存分页直接导致JVM内存溢出。正确的做法应该是利用数据库的天然分页能力配合精心设计的索引策略。2. 分页查询的深度优化2.1 基于游标的分页策略传统LIMIT分页在数据量大时性能会急剧下降。实测显示当查询第100页每页20条时MySQL需要扫描前2000条记录。在我的项目中改用基于createTime的游标分页后查询耗时稳定在50ms以内TaskQuery taskQuery taskService.createTaskQuery() .active() .taskCreatedAfter(lastTaskCreateTime) // 游标锚点 .orderByTaskCreateTime() .asc() .limit(pageSize);关键优化点使用taskCreatedAfter替代offset避免全表扫描配合(task_create_time, id)的联合索引前端保存最后一条记录的createTime作为下一页请求参数2.2 多维度权限过滤候选人场景下的查询需要特别注意。早期版本我们使用了多个OR条件taskQuery.or() .taskAssignee(userId) .taskCandidateUser(userId) .taskCandidateGroupIn(roleIds) .endOr();后来发现这种写法会导致索引失效。优化方案是拆分为三个独立查询再合并结果并通过Redis缓存权限数据ListTask assignedTasks queryAssignedTasks(userId); ListTask candidateTasks queryCandidateTasks(userId); ListTask groupTasks queryGroupTasks(roleIds);3. 状态机设计实战3.1 状态建模待办任务的生命周期可以用状态机清晰定义。这是我们项目中使用的状态转换图当前状态触发动作下一状态业务规则校验UNCLAIMEDCLAIMASSIGNED用户必须在候选人列表ASSIGNEDCOMPLETEDONE必须填写审批意见ASSIGNEDRETURNUNCLAIMED需记录归还原因3.2 状态机实现使用Spring StateMachine的配置示例Configuration EnableStateMachine public class TaskStateMachineConfig extends EnumStateMachineConfigurerAdapterTaskState, TaskEvent { Override public void configure(StateMachineStateConfigurerTaskState, TaskEvent states) { states.withStates() .initial(TaskState.UNCLAIMED) .state(TaskState.ASSIGNED) .end(TaskState.DONE); } Override public void configure(StateMachineTransitionConfigurerTaskState, TaskEvent transitions) { transitions .withExternal() .source(TaskState.UNCLAIMED) .target(TaskState.ASSIGNED) .event(TaskEvent.CLAIM) .guard(new TaskClaimGuard()) // 权限校验 .and() .withExternal() .source(TaskState.ASSIGNED) .target(TaskState.DONE) .event(TaskEvent.COMPLETE); } }踩坑提醒一定要在状态变更时持久化操作日志。我们曾因缺少日志记录无法追溯某关键任务被异常归还的原因最后只能通过数据库binlog恢复现场。4. 性能压测与调优4.1 缓存策略设计对于高频访问的流程定义数据采用二级缓存方案本地Caffeine缓存50ms过期应对突发流量Redis集群缓存5分钟过期保证多实例一致性关键配置示例Bean public CacheManager cacheManager() { CaffeineCacheManager manager new CaffeineCacheManager(); manager.registerCustomCache(processDefCache, Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(50, TimeUnit.MILLISECONDS) .build()); return manager; }4.2 数据库优化针对Flowable的ACT_RU_TASK表建议添加以下索引CREATE INDEX idx_task_tenant ON ACT_RU_TASK (TENANT_ID_, ASSIGNEE_); CREATE INDEX idx_task_candidate ON ACT_RU_TASK (TENANT_ID_, PROC_DEF_ID_) WHERE ASSIGNEE_ IS NULL;压测数据显示在10万级任务数据量下优化后的查询性能提升约8倍场景QPS平均响应时间错误率原始方案120450ms1.2%优化方案98055ms0%5. 前端渲染优化对于任务列表这种高频操作的界面我推荐使用虚拟滚动技术。在Vue3中的实现示例template div classviewport scrollhandleScroll div classlist :style{ height: totalHeight px } div v-foritem in visibleItems :keyitem.taskId :style{ transform: translateY(${item.offset}px) } !-- 任务项内容 -- /div /div /div /template script setup const itemHeight 60; const bufferSize 10; const visibleCount Math.ceil(viewportHeight / itemHeight); const visibleItems computed(() { const start Math.max(0, scrollTop.value / itemHeight - bufferSize); const end start visibleCount bufferSize * 2; return taskList.slice(start, end).map((item, i) ({ ...item, offset: (start i) * itemHeight })); }); /script这种方案在3000条任务的测试中DOM节点数始终保持在30个左右滚动流畅度提升明显。记得在表格列中使用固定的min-width避免浏览器频繁重排。
Flowable7.x实战指南:构建高性能待办列表的分页策略与状态机设计
发布时间:2026/5/16 1:47:44
1. 高性能待办列表的架构挑战在SaaS平台中构建待办列表功能时我们常常会遇到几个典型痛点。最近接手的一个多租户项目里当并发用户数超过500时系统响应时间从原来的200ms飙升到2秒以上这让我不得不重新思考待办列表的设计方案。核心瓶颈往往出现在三个方面首先是数据库查询效率当用户任务量达到万级时简单的LIMIT分页会导致深度翻页性能骤降其次是权限过滤的复杂度特别是候选人、候选组这类动态权限场景最后是状态管理的混乱比如任务在待办、拾取、归还等状态间的流转缺乏统一管控。我见过最糟糕的实现是把所有任务数据加载到内存中处理。某次事故排查发现一个租户管理员查询待办列表时后端竟然加载了全公司3万多条任务记录进行内存分页直接导致JVM内存溢出。正确的做法应该是利用数据库的天然分页能力配合精心设计的索引策略。2. 分页查询的深度优化2.1 基于游标的分页策略传统LIMIT分页在数据量大时性能会急剧下降。实测显示当查询第100页每页20条时MySQL需要扫描前2000条记录。在我的项目中改用基于createTime的游标分页后查询耗时稳定在50ms以内TaskQuery taskQuery taskService.createTaskQuery() .active() .taskCreatedAfter(lastTaskCreateTime) // 游标锚点 .orderByTaskCreateTime() .asc() .limit(pageSize);关键优化点使用taskCreatedAfter替代offset避免全表扫描配合(task_create_time, id)的联合索引前端保存最后一条记录的createTime作为下一页请求参数2.2 多维度权限过滤候选人场景下的查询需要特别注意。早期版本我们使用了多个OR条件taskQuery.or() .taskAssignee(userId) .taskCandidateUser(userId) .taskCandidateGroupIn(roleIds) .endOr();后来发现这种写法会导致索引失效。优化方案是拆分为三个独立查询再合并结果并通过Redis缓存权限数据ListTask assignedTasks queryAssignedTasks(userId); ListTask candidateTasks queryCandidateTasks(userId); ListTask groupTasks queryGroupTasks(roleIds);3. 状态机设计实战3.1 状态建模待办任务的生命周期可以用状态机清晰定义。这是我们项目中使用的状态转换图当前状态触发动作下一状态业务规则校验UNCLAIMEDCLAIMASSIGNED用户必须在候选人列表ASSIGNEDCOMPLETEDONE必须填写审批意见ASSIGNEDRETURNUNCLAIMED需记录归还原因3.2 状态机实现使用Spring StateMachine的配置示例Configuration EnableStateMachine public class TaskStateMachineConfig extends EnumStateMachineConfigurerAdapterTaskState, TaskEvent { Override public void configure(StateMachineStateConfigurerTaskState, TaskEvent states) { states.withStates() .initial(TaskState.UNCLAIMED) .state(TaskState.ASSIGNED) .end(TaskState.DONE); } Override public void configure(StateMachineTransitionConfigurerTaskState, TaskEvent transitions) { transitions .withExternal() .source(TaskState.UNCLAIMED) .target(TaskState.ASSIGNED) .event(TaskEvent.CLAIM) .guard(new TaskClaimGuard()) // 权限校验 .and() .withExternal() .source(TaskState.ASSIGNED) .target(TaskState.DONE) .event(TaskEvent.COMPLETE); } }踩坑提醒一定要在状态变更时持久化操作日志。我们曾因缺少日志记录无法追溯某关键任务被异常归还的原因最后只能通过数据库binlog恢复现场。4. 性能压测与调优4.1 缓存策略设计对于高频访问的流程定义数据采用二级缓存方案本地Caffeine缓存50ms过期应对突发流量Redis集群缓存5分钟过期保证多实例一致性关键配置示例Bean public CacheManager cacheManager() { CaffeineCacheManager manager new CaffeineCacheManager(); manager.registerCustomCache(processDefCache, Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(50, TimeUnit.MILLISECONDS) .build()); return manager; }4.2 数据库优化针对Flowable的ACT_RU_TASK表建议添加以下索引CREATE INDEX idx_task_tenant ON ACT_RU_TASK (TENANT_ID_, ASSIGNEE_); CREATE INDEX idx_task_candidate ON ACT_RU_TASK (TENANT_ID_, PROC_DEF_ID_) WHERE ASSIGNEE_ IS NULL;压测数据显示在10万级任务数据量下优化后的查询性能提升约8倍场景QPS平均响应时间错误率原始方案120450ms1.2%优化方案98055ms0%5. 前端渲染优化对于任务列表这种高频操作的界面我推荐使用虚拟滚动技术。在Vue3中的实现示例template div classviewport scrollhandleScroll div classlist :style{ height: totalHeight px } div v-foritem in visibleItems :keyitem.taskId :style{ transform: translateY(${item.offset}px) } !-- 任务项内容 -- /div /div /div /template script setup const itemHeight 60; const bufferSize 10; const visibleCount Math.ceil(viewportHeight / itemHeight); const visibleItems computed(() { const start Math.max(0, scrollTop.value / itemHeight - bufferSize); const end start visibleCount bufferSize * 2; return taskList.slice(start, end).map((item, i) ({ ...item, offset: (start i) * itemHeight })); }); /script这种方案在3000条任务的测试中DOM节点数始终保持在30个左右滚动流畅度提升明显。记得在表格列中使用固定的min-width避免浏览器频繁重排。