资讯动态

Activiti HistoryService 实战:从历史数据查询到流程审计与清理

发布时间:2026/10/14 18:17:04 来源:尧图企业网站定制
做工作流项目这几年我越来越觉得 Activiti 里最容易被低估的就是 HistoryService。很多人刚上手时把重心全放在 RuntimeService、TaskService 上流程能跑通就以为万事大吉结果一到“复盘流程”就傻眼了——某个单子走了多久、每个节点是谁处理的、中间变量到底变成什么样全都说不清。HistoryService 就是专门干这个事的它把流程实例、任务、活动、变量这些运行期数据沉淀成历史数据让你随时能回溯整个流程的完整生命周期。这篇文章我按自己学习 HistoryService 的路径来写先讲清楚它到底管哪些数据、历史级别该怎么配再逐个拆解核心查询 API然后用一个请假流程的完整 Demo 把增删查整个跑一遍最后是几个实际项目里踩过的坑和清理思路。适合正在学 Activiti 的初级开发者也适合那种已经能用 RuntimeService 跑流程、但一碰历史查询就一头雾水的人。1. 先搞明白 HistoryService 到底管什么1.1 历史数据不只是“日志”是流程审计的底座Activiti 的数据库表分成两大类一类是运行期表前缀是ACT_RU_比如ACT_RU_EXECUTION、ACT_RU_TASK另一类是历史表前缀是ACT_HI_。很多人只盯着运行期表看但运行期表里的数据本质上是一种“瞬时状态”。什么意思呢一个流程实例还在跑的时候你确实可以在ACT_RU_EXECUTION里看到它当前的执行状态在ACT_RU_TASK里看到谁正在处理哪个任务。但一旦流程走完甚至被删除这些记录就会跟着消失。运行期表的设计目标是“让流程现在能跑得快、跑得对”它不会给你保留一段完整的历史。HistoryService 对应的是ACT_HI_*这一组表记录的是已经发生的事情。它的核心价值有三块流程维度的复盘某个流程实例什么时候开始、什么时候结束、总共耗时多久。任务维度的追溯每个任务谁处理的、处理了多久、审批结论是什么。数据维度的审计流程里的业务变量怎么变化的每次更新发生在哪个节点。说白了运行期数据是“当下”历史数据是“真相”。业务上做审计、做统计报表、做流程优化分析全靠这些历史记录撑着。我碰到过不止一个项目前期开发根本不在乎历史数据等系统上线三个月后财务要拉一份“各审批节点平均耗时”的报表开发才发现历史数据压根没配得回填数据那叫一个痛苦。1.2 HistoryService 在整个引擎里的位置与调用方式HistoryService 是 Activiti 五大核心服务之一。这里顺手列一下我日常最常用的几个服务方便你把它的位置看清楚服务管什么RepositoryService流程定义、BPMN 模型、部署包的管理RuntimeService流程实例的启动、推进、执行、消息触发TaskService人工任务的创建、认领、完成、委派HistoryService所有历史数据的查询与删除ManagementService引擎内部命令、Job 执行、表结构维护历史数据并不是 HistoryService 自己产生的它是引擎在执行流程过程中自动落库的。也就是说你在流程运行阶段照常用 RuntimeService、TaskService引擎会在背后把历史记录写进ACT_HI_*表HistoryService 负责让你把这些沉淀下来的数据查出来用。调用方式非常简单先从引擎对象上拿服务ProcessEngine processEngine ProcessEngines.getDefaultProcessEngine(); HistoryService historyService processEngine.getHistoryService();如果你用的是 Spring Boot 集成直接注入HistoryService就行。有一点我想强调HistoryService 本身提供的写操作非常有限主要就是历史数据的删除和清理日常你碰得最多的还是它那一大批createXxxQuery()方法。后面的篇幅我基本都围绕这些查询方法来讲。2. 历史级别配置能不能查历史从一开始就定了2.1 四个级别分别记什么这是很多新手最容易忽略但影响最大的一步。Activiti 的历史记录是有开关控制的叫做 HistoryLevel一共四个值级别记录内容默认适用场景none什么都不记否对历史完全没要求、追求极致性能activity流程实例 活动实例否只想知道流程走到过哪些节点auditactivity 级别之上再加任务与变量实例只保留变量最后一次值是从 5.12 起常规业务审计、绝大多数项目fullaudit 之上再加变量更新的完整明细每次变更都留痕否合规审计、需要完整追踪数据变化这里要解释清楚“变量实例”和“变量更新明细”的区别很多博客没讲透。在 audit 级别下ACT_HI_VARINST表里会存一个变量当前的最终值比如审批金额最后变成了 5000那你在历史变量表里只能查到 5000。但在 full 级别下ACT_HI_DETAIL表会把所有变量变更的每次快照都记下来一开始填的是 3000后来改成了 5000每一步都能查到连变更时间、发生在哪个节点都能对上。所以我给项目的建议一直是凡是跟钱、权限、合同这类敏感业务挂钩的流程历史级别直接上 full别省。这个开关成本很低但补历史数据几乎不可能别给自己埋雷。2.2 配置写法与切换注意事项如果你用纯 Java 方式创建引擎可以这么配ProcessEngineConfiguration cfg ProcessEngineConfiguration .createStandaloneProcessEngineConfiguration() .setJdbcUrl(jdbc:mysql://localhost:3306/activiti) .setJdbcUsername(root) .setJdbcPassword(123456) .setHistoryLevel(HistoryLevel.FULL); ProcessEngine processEngine cfg.buildProcessEngine();如果你用 Spring Boot在application.yml里配置即可spring: activiti: history-level: full配置本身没什么难度但有三点坑要提醒你。第一历史级别要在引擎第一次初始化之前想清楚。引擎首次启动时会根据这个配置来创建ACT_HI_*表如果表已经存在你再从none切到full引擎并不会帮你补建字段或补记历史生产环境里来回切换级别很容易出现“查得到活动但查不到变量”这种诡异现象。第二这个配置只对切换之后新产生的流程生效。存量流程不会因为你改配置就补上历史这个心理预期要有。第三级别越高写入量越大。full 级别下每次setVariable都会往ACT_HI_DETAIL写一条记录高频变更的流程如果并发又大历史表会膨胀得很快。后面第五节我会专门说清理策略。3. 核心查询 API 逐个拆解3.1 历史流程实例查过程也查结果先从我平时用得最多的HistoricProcessInstanceQuery说起。它查的是ACT_HI_PROCINST表一条记录对应一个跑过的流程实例。我常用的玩法有几种。查所有已完成的流程实例按结束时间倒序ListHistoricProcessInstance list historyService .createHistoricProcessInstanceQuery() .finished() .orderByProcessInstanceEndTime() .desc() .list();查某个流程定义下未完成的流程实例业务上通常用来做“当前有哪些单子在途”的统计ListHistoricProcessInstance runningList historyService .createHistoricProcessInstanceQuery() .processDefinitionKey(leave) .unfinished() .list();按流程实例 ID 精确查询单条HistoricProcessInstance instance historyService .createHistoricProcessInstanceQuery() .processInstanceId(processInstanceId) .singleResult();HistoricProcessInstance对象上最有用的字段是 startTime、endTime、durationInMillis还有 businessKey。durationInMillis是引擎自动算好的直接拿来做平均耗时统计非常方便不用自己用时间戳减。这里有个细节processInstanceId和processDefinitionKey不要混。前者是一次具体运行的身份后者是流程模板的身份。一个流程定义可以被启动几百次对应几百条历史流程实例记录。3.2 历史任务实例人维度的干活记录流程跑过之后业务上问得最多的一句话是“这个单子是谁审的”。答案在HistoricTaskInstanceQuery里对应表ACT_HI_TASKINST。按审批人查他处理过的任务ListHistoricTaskInstance tasks historyService .createHistoricTaskInstanceQuery() .taskAssignee(zhangsan) .finished() .orderByHistoricTaskInstanceEndTime() .desc() .list();查某个流程实例下的所有历史任务ListHistoricTaskInstance taskList historyService .createHistoricTaskInstanceQuery() .processInstanceId(processInstanceId) .orderByHistoricTaskInstanceStartTime() .asc() .list();任务表里的ACTIVITI_HI_TASKINST和ACT_HI_ACTINST很多人搞混。简单记任务表是“人办的事”记录的是 userTask 节点的分配和处理包含办理人、开始时间、结束时间、耗时、审批意见。活动表是“引擎走过的路”记录流程里每一个节点包括开始事件、结束事件、排他网关、服务任务的执行情况。实际项目中统计“人均办结时长”“各节点耗时分布”这种需求用 HistoricTaskInstance 最顺手因为引擎已经帮你把 assignee、duration 这些维度拆好了。3.3 历史活动实例节点维度的执行轨迹HistoricActivityInstanceQuery对应ACT_HI_ACTINST表。我通常用它来画流程执行的时序图排查“流程卡在哪个节点”之类的问题。查流程实例经过的所有节点ListHistoricActivityInstance activities historyService .createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .orderByHistoricActivityInstanceStartTime() .asc() .list(); for (HistoricActivityInstance act : activities) { System.out.println(act.getActivityType() | act.getActivityName() | act.getStartTime() | act.getEndTime()); }这里返回的活动记录包含开始节点、结束节点、各种网关如果你只想看用户任务节点可以加一个过滤条件.hactivityType(userTask)排查线上问题的时候我一般在活动表里按流程实例 ID 把完整节点轨迹拉出来看看哪个节点 startTime 有值但 endTime 为空。如果有说明流程实例在这个节点停留了很久大概率是任务没处理或者异步作业没跑完。3.4 历史变量与明细数据变化追踪历史变量查询有两个入口分别对应“最终值”和“变更轨迹”。查历史变量最终值ListHistoricVariableInstance variableList historyService .createHistoricVariableInstanceQuery() .processInstanceId(processInstanceId) .list(); for (HistoricVariableInstance var : variableList) { System.out.println(var.getVariableName() var.getValue()); }查完整变更轨迹需要流程级别是 fullListHistoricDetail details historyService .createHistoricDetailQuery() .processInstanceId(processInstanceId) .orderByTime() .asc() .list(); for (HistoricDetail detail : details) { if (detail instanceof HistoricVariableUpdate) { HistoricVariableUpdate update (HistoricVariableUpdate) detail; System.out.println(update.getVariableName() - update.getValue() detail.getTime()); } }HistoricVariableUpdate里还有一个很关键的字段activityInstanceId它告诉你这次变量修改发生在哪个节点。配合活动实例查询就能还原出“变量在某节点被修改”的完整链路。做合规审计时这个能力非常关键。4. 从请假流程跑通 HistoryService 全流程4.1 准备模型与部署理论讲再多不如跑一个完整 Demo。我拿一个最经典的请假流程来演示流程结构非常简单开始事件 - 提交请假申请userTask- 部门经理审批userTask- 人事归档userTask- 结束事件。我不用图形化工具直接用代码部署流程定义方便你照抄复现。先写一个极简 BPMN 文件放在资源目录下?xml version1.0 encodingUTF-8? definitions xmlnshttp://www.omg.org/spec/BPMN/20100524/MODEL xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance targetNamespacehttp://example.com/leave process idleave name请假流程 isExecutabletrue startEvent idstartEvent name开始/ sequenceFlow idsf1 sourceRefstartEvent targetRefapplyTask/ userTask idapplyTask name提交请假申请 activiti:assignee${applyUser}/ sequenceFlow idsf2 sourceRefapplyTask targetRefmanagerTask/ userTask idmanagerTask name部门经理审批 activiti:assignee${managerUser}/ sequenceFlow idsf3 sourceRefmanagerTask targetRefarchiveTask/ userTask idarchiveTask name人事归档 activiti:assignee${hrUser}/ sequenceFlow idsf4 sourceRefarchiveTask targetRefendEvent/ endEvent idendEvent name结束/ /process /definitions部署这段 XML 的代码Autowired private RepositoryService repositoryService; Autowired private RuntimeService runtimeService; Autowired private TaskService taskService; Autowired private HistoryService historyService; public void deployLeaveProcess() { repositoryService.createDeployment() .addClasspathResource(processes/leave.bpmn20.xml) .name(请假流程) .deploy(); }4.2 启动流程并完成任务部署完成后启动一个真实流程实例带上业务变量MapString, Object variables new HashMap(); variables.put(applyUser, zhangsan); variables.put(managerUser, lisi); variables.put(hrUser, wangwu); variables.put(applyDays, 3); variables.put(approveResult, ); ProcessInstance processInstance runtimeService .startProcessInstanceByKey(leave, LEAVE-2025-001, variables);这里startProcessInstanceByKey(leave)用的是流程定义 ID第二个参数是业务 Key这个 Key 在历史查询里可以直接关联你业务系统的单号强烈建议项目里都用上。接下来把三个人工任务依次完成模拟整个审批链路走完// zhangsan 提交申请 Task applyTask taskService.createTaskQuery() .processInstanceId(processInstance.getId()) .taskAssignee(zhangsan) .singleResult(); MapString, Object applyVars new HashMap(); applyVars.put(leaveReason, 老家有事); taskService.complete(applyTask.getId(), applyVars); // lisi 经理审批 Task managerTask taskService.createTaskQuery() .processInstanceId(processInstance.getId()) .taskAssignee(lisi) .singleResult(); MapString, Object managerVars new HashMap(); managerVars.put(approveResult, 同意); managerVars.put(approveRemark, 准假三天); taskService.complete(managerTask.getId(), managerVars); // wangwu 人事归档 Task archiveTask taskService.createTaskQuery() .processInstanceId(processInstance.getId()) .taskAssignee(wangwu) .singleResult(); taskService.complete(archiveTask.getId());这一步有个容易踩的坑TaskQuery的taskAssignee方法是精确匹配办理人。如果任务还没被认领你是查不到结果的。实际项目里任务分配方式五花八门有人用 candidate 候选组有人直接在流程变量里放办理人这里为了演示清晰我用流程变量直接指定 assignee最简单直观。4.3 历史数据查询与解读流程走完后所有历史记录应该已经沉淀到库里了。现在用 HistoryService 把这些数据捞出来看。查流程实例维度HistoricProcessInstance hpi historyService .createHistoricProcessInstanceQuery() .processInstanceId(processInstance.getId()) .singleResult(); System.out.println(业务Key: hpi.getBusinessKey()); System.out.println(开始时间: hpi.getStartTime()); System.out.println(结束时间: hpi.getEndTime()); System.out.println(总耗时: hpi.getDurationInMillis() ms);查任务维度按时间正序排ListHistoricTaskInstance htList historyService .createHistoricTaskInstanceQuery() .processInstanceId(processInstance.getId()) .orderByHistoricTaskInstanceStartTime() .asc() .list(); for (HistoricTaskInstance ht : htList) { System.out.println(ht.getName() | ht.getAssignee() | ht.getStartTime() - ht.getEndTime()); }查活动维度看整个流程的轨迹ListHistoricActivityInstance haList historyService .createHistoricActivityInstanceQuery() .processInstanceId(processInstance.getId()) .orderByHistoricActivityInstanceStartTime() .asc() .list(); for (HistoricActivityInstance ha : haList) { String end ha.getEndTime() null ? 进行中 : ha.getEndTime().toString(); System.out.println(ha.getActivityType() | ha.getActivityName() | ha.getStartTime() - end); }查历史变量最终值ListHistoricVariableInstance hviList historyService .createHistoricVariableInstanceQuery() .processInstanceId(processInstance.getId()) .list(); for (HistoricVariableInstance hvi : hviList) { System.out.println(hvi.getVariableName() hvi.getValue()); }如果历史级别是 full还可以用createHistoricDetailQuery()把所有变量变更轨迹拉出来你会看到leaveReason是在“提交请假申请”节点写入的approveResult是在“部门经理审批”节点写入的每一步都有据可查。跑完这个 Demo 你会发现HistoryService 的查询模式非常统一createXxxQuery()返回一个 Query 对象然后不断拼条件最后用list()、singleResult()、count()收尾。跟 MyBatis 的 QueryWrapper 是同一个套路学会一种其他几个查询就是换对象名的事。5. 常见问题排查与性能优化思路5.1 历史数据查不到先别怀疑代码很多人在群里问“为什么 HistoryService 查不到数据”我第一反应永远是先查历史级别是不是 none。因为很多人从网上复制了配置history-level这个配置项在旧版本里叫history复制错了地方或者压根没配引擎就会用默认的 audit 级别。如果业务对历史要求不高audit 其实够用但如果你以为自己配了 full实际却是 audit查变量变更轨迹时就会少数据表现就是“Detail 查询结果为空”。第二类常见问题流程实例还在运行期你却按“已完成”条件去查。比如用.finished()过滤流程没走完当然查不出来换成.unfinished()或直接去掉状态过滤就能看到数据了。第三类是事务问题。流程执行和历史写入在同一个事务里如果你的代码在事务提交前就去查历史大概率查不到刚写入的数据。我自己用单元测试时踩过好几次。解决办法很简单查询放在事务提交之后或者干脆在测试方法上加Transactional配合Rollback(false)先把数据真正提交到库里再验证。5.2 流程删除后历史记录怎么办Activiti 的删除策略要区分清楚。RuntimeService.deleteProcessInstance()删除的是运行中的流程实例它也会级联干掉相关任务但历史记录里通常还会保留一条“未正常结束”的痕迹这个设计是刻意的方便你追溯“哪些流程是被强行终止的”。如果你想彻底清掉某条历史记录用 HistoryServicehistoryService.deleteHistoricProcessInstance(processInstanceId);这个操作会删掉该流程实例相关的活动、任务、变量等所有历史数据。注意两点一是如果你删的是运行中的流程实例要先调 RuntimeService 把运行期数据干掉否则历史删除会报错或留下脏数据二是这个删除操作是不可逆的生产环境最好包一层逻辑判断甚至做成定时批量任务别在代码里裸调。对于已经归档完成的历史记录很多团队会做定期清理。我们项目的做法是生产库保留半年内的历史数据超过半年的按月归档到独立的历史库再通过定时任务删除主库记录。查询的时候默认只查半年内归档需求走独立查询接口。5.3 历史表数据膨胀与清理策略历史表膨胀是工作流入场后三个月到半年必然遇到的坎。尤其是 full 级别下ACT_HI_DETAIL是增长最凶的表。我见过一个日审批量几百条的中型系统半年跑下来ACT_HI_DETAIL上千万行查询明显变慢。优化思路按优先级排我建议这么来第一优先确认索引是否齐全。ACT_HI_PROCINST上的PROC_INST_ID_、PROC_DEF_KEY_ACT_HI_TASKINST上的PROC_INST_ID_、ASSIGNEE_、START_TIME_ACT_HI_DETAIL上的PROC_INST_ID_、ACT_INST_ID_这些字段是查询高频条件一旦全表扫千万级数据直接卡到秒级超时。没有索引的列该建就建。第二优先业务上区分“热数据”和“冷数据”。运行中的流程实例必然有历史记录这部分是热数据。已结束三个月以上的流程基本不会有人天天查除非有专项审计。把历史查询接口默认加一个结束时间范围从根源上减少扫描数据量。不要在createHistoricTaskInstanceQuery()上查全表然后内存里 filter这种写法数据一多就废。第三优先定期清理任务。写一个定时清理的 Service按月删除过期历史。删除顺序要讲究先删明细表ACT_HI_DETAIL再删变量表ACT_HI_VARINST然后删活动表ACT_HI_ACTINST最后删任务表ACT_HI_TASKINST和流程实例表ACT_HI_PROCINST。如果你有外键关联直接先删子表再删主表否则会卡在约束上。分享一个我踩过的坑一开始清理历史数据用的是全表 delete量一大就锁表把线上业务都拖慢了。后来改成按批次删除每一批只删 5000 条然后循环执行。相同的数据量对线上影响小了一个数量级。凡是删历史数据的任务一定要控制批次大小千万别一次性 delete 几十万行。写在后面的经验HistoryService 看起来查询 API 就那么几个真正用好的关键在于你想清楚“业务要回溯什么”。如果只是做流量统计activity 级别就够了如果要做节点耗时分析audit 是底线如果要给合规审计交差别犹豫直接 full并且从一开始就把变量变更轨迹接入业务日志系统。我个人的体会是做工作流项目不要等到报表需求来了才看历史配置。产品需求评审的时候只要听到“统计”“审计”“报表”“轨迹”任何一个词你就应该立刻想到 HistoryService并且在技术设计文档里把历史级别、归档策略、清理任务这三件事写清楚。前期多做十分钟设计后面能省好几个加班的夜。最后再分享一个小技巧开发环境里验证历史查询结果时别光靠日志输出对象字段。直接连上数据库查ACT_HI_PROCINST、ACT_HI_TASKINST这几张表你能同时看到引擎到底往库里写了什么排查问题的效率会翻倍。把表结构和 API 返回字段对照着看你对 Activiti 历史模块的理解会比单纯读代码快得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑