首页 / 资讯中心 / 文章详情

Flowable工作流引擎数据库访问机制全解析:MyBatis整合、表结构与性能优化

Flowable工作流引擎数据库访问机制全解析:MyBatis整合、表结构与性能优化 ★ FEATURED ARTICLE
Flowable 工作流引擎的 DB 访问层说白了就是整个引擎的记账本和状态机底座。你写流程定义、发起流程实例、做任务审批、查历史轨迹最终都会落到一张张表里。很多人用 Flowable 能跑通 Demo但一遇到性能问题、数据不一致、或者想定制流程引擎行为就卡在“不知道它到底怎么操作数据库”这层窗户纸上。这篇就把 Flowable 的 DB 访问机制从头到尾拆一遍包括 MyBatis 的整合方式、表结构设计逻辑、缓存机制、SQL 执行链路以及你在二次开发时最容易踩的坑。1. 内容整体设计与思路拆解Flowable 的持久化层不是自己造轮子而是直接站在 MyBatis 的肩膀上。这个选型在当年 Activiti 分家的时候延续了下来后来 Flowable 团队也一直没有换原因很现实MyBatis 够轻、够灵活SQL 完全由自己掌控对于工作流这种“大量复杂查询 高频单行更新”的场景比 Hibernate 这类全自动 ORM 要可控得多。1.1 核心需求解析先搞清楚 Flowable 的 DB 访问到底要解决什么问题流程定义的存储与版本管理你部署一个 BPMN 文件引擎要把 XML、流程图、表单 key 等信息落库并且支持同一个 key 多次部署生成不同版本。流程实例与执行实例的状态管理一个流程实例从发起走到结束中间会经历多个执行实例Execution每个执行实例又关联当前节点、父节点、子节点这是一张典型的多叉树结构。任务与任务的候选/办理人管理用户任务UserTask的创建、认领、办理、转办、委派以及候选人、候选组的关系维护。变量与局部变量的存取流程实例级的变量Variable和执行实例级的局部变量LocalVariable要支持序列化存储。历史数据的归档与查询所有已经完成的任务、活动、流程实例都要落到历史表支撑流程追溯和统计。运行时与历史的分离运行时表ACT_RU_存的是“正在跑”的数据历史表ACT_HI_存的是“跑完”的数据两者通过 ID 关联这套设计是 Flowable 能长期稳定运行的核心。1.2 为什么选 MyBatis 而不是 JPA这里多说一句选型逻辑。工作流引擎最怕什么最怕查询不可控。JPA 帮你自动生成 SQL在单表 CRUD 时确实省事但工作流的查询往往是多表关联、动态条件、分页排序JPA 的 Criteria API 写起来又长又难调优。而 MyBatis 允许你把每条 SQL 都亲手写出来想怎么 join 就怎么 join想怎么用索引就怎么用索引。再有一点Flowable 需要兼容多种数据库MySQL、Oracle、PostgreSQL、SQL Server、H2 等每种数据库的分页语法、主键生成策略、日期函数都不一样。MyBatis 提供了数据库方言DatabaseIdProvider机制Flowable 内部针对不同数据库准备了多套 SQL 映射文件这也是它能做到“一套代码跑多种库”的关键原因。2. 核心细节解析与实操要点2.1 Flowable 数据库表结构全景Flowable 的表名前缀是ACT_后面跟两个字母表示分类。你把 Flowable 跑起来后会自动创建几十张表按功能可以分成五大类分类前缀作用典型表通用数据ACT_GE_公共数据如属性配置、二进制数据ACT_GE_PROPERTY, ACT_GE_BYTEARRAY流程定义ACT_RE_Repository流程定义与部署包ACT_RE_DEPLOYMENT, ACT_RE_PROCDEF运行时数据ACT_RU_Runtime正在执行的流程实例、任务、变量等ACT_RU_EXECUTION, ACT_RU_TASK, ACT_RU_VARIABLE, ACT_RU_IDENTITYLINK, ACT_RU_JOB历史数据ACT_HI_History已结束的流程、任务、活动等ACT_HI_PROCINST, ACT_HI_TASKINST, ACT_HI_ACTINST, ACT_HI_VARINST其他ACT_EVT_事件订阅、定时器等ACT_EVT_LOG, ACT_RU_TIMER_JOB这套表结构的设计逻辑可以这样理解ACT_RU_*是“内存态数据落库”进程一结束或者流程一完成这些记录就会被清理或移到历史表ACT_HI_*是“永久存档”只增不改。2.2 核心表的作用与关联方式拿一次完整的请假流程举例。你提交请假申请后ACT_RE_DEPLOYMENT增加一条部署记录ACT_RE_PROCDEF增加一条流程定义记录ACT_GE_BYTEARRAY存 BPMN XML 和流程图 PNG。发起流程后ACT_RU_EXECUTION会插入一条根执行实例记录ACT_RU_TASK插入一条待办任务记录。如果你在流程里设置了流程变量比如请假天数、审批意见ACT_RU_VARIABLE会新增记录。任务办理完成后相关记录会转移到ACT_HI_TASKINST、ACT_HI_ACTINST运行时表的记录被删除。整个流程结束后ACT_HI_PROCINST会记录结束时间、持续时长、结束原因。这中间最关键的关联字段是PROC_INST_ID_和EXECUTION_ID_。任务表通过PROC_INST_ID_关联流程实例通过EXECUTION_ID_关联到具体的执行分支。在排他网关分裂出多个并行分支时一个流程实例会有多个ACT_RU_EXECUTION记录但PROC_INST_ID_都是同一个。2.3 运行时常驻数据与缓存机制你可能好奇Flowable 每次查任务列表都要查数据库吗不完全是。Flowable 在一个流程实例的完整生命周期内会尽量把相关的实体对象缓存到EntityCache里。这个缓存以Object为粒度以Class ID为 key只要同一事务还没提交重复查询同一个实体不会产生新的 SQL。这个设计跟 Hibernate 的一级缓存有点类似但实现更轻。你在同一个事务里连续查两次同一个任务Task task1 taskService.createTaskQuery().taskId(123).singleResult(); Task task2 taskService.createTaskQuery().taskId(123).singleResult();第二次查询走的就是缓存不会发 SQL。这在高频审批场景下能省下不少数据库压力。但有个坑Flowable 的缓存是事务级缓存事务提交后缓存就失效了。如果你在同一个事务里修改了流程变量然后又去查询列表接口拿到的是更新后的值但如果你在一个长事务里先查询后修改再在后续代码里用之前查出来的对象做判断可能会得到过期数据。所以在自定义命令Command里面操作流程数据时修改后务必通过Context.getCommandContext().getTaskEntityManager()重新获取实体而不是持有旧引用。3. 实操过程与核心环节实现3.1 Flowable 启动时的表自动创建逻辑Flowable 的ProcessEngineConfiguration里有个databaseSchemaUpdate配置配置值行为适用场景false不自动建表启动时校验表结构生产环境true启动时自动建表如果表存在则不做处理开发测试环境create-drop启动时建表关闭时删表单元测试实际生产建议你设置成false用 Flowable 自带的 SQL 脚本手动建表。这样做的好处是升级版本时能明确知道表结构变更不会因为自动建表把已有数据弄坏。如果你用的是 Spring Boot 整合配置是这样spring: datasource: url: jdbc:mysql://localhost:3306/flowable?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver flowable: database-schema-update: true async-executor-activate: false启动时 Flowable 会通过DbSqlSession执行建表脚本。它会先查ACT_GE_PROPERTY表判断数据库里是否已经有 Flowable 的元数据如果没有就按数据库类型加载对应的flowable.mysql.create.engine.sql脚本执行。注意如果你用的是 MySQL 5.7 以下版本Flowable 6.x 默认使用utf8mb4字符集建议提前把库和表的字符集都设置成utf8mb4不然存中文日志或变量值的时候会报错。3.2 关键 SQL 的定位方法想理解 Flowable 的 DB 访问你得知道它的 MyBatis 映射文件放在哪。以 Flowable 6.7.2 为例在flowable-enginejar 包里的路径是org/flowable/db/mapping/entity/你会看到有一堆*.xml文件命名规则是表名.xml比如Task.xmlExecution.xmlHistoricActivityInstance.xmlVariableInstance.xml这些 XML 里定义了所有针对该实体的增删改查 SQL。比如Task.xml里有一个selectTaskByQueryCriteria这就是taskService.createTaskQuery().list()底层执行的 SQL。它包含了大量的动态条件判断select idselectTaskByQueryCriteria parameterTypeorg.flowable.task.service.impl.TaskQueryImpl resultMaptaskResultMap if testtaskId ! null and RES.ID_ #{taskId} /if if testname ! null and RES.NAME_ #{name} /if if testprocessInstanceId ! null and RES.PROC_INST_ID_ #{processInstanceId} /if ... /select这里有个调试技巧当你发现某个查询慢可以直接在 MyBatis 的日志配置里打开 SQL 输出把实际执行的 SQL 捞出来然后用EXPLAIN分析索引。Flowable 的查询大部分都是按ID_、PROC_INST_ID_、TASK_DEF_KEY_走索引最容易出问题的是按候选人ACT_RU_IDENTITYLINK关联查询任务列表一旦数据量大不加索引会很痛苦。3.3 通过自定义 Command 注入 DB 操作Flowable 提供了一个很强大的扩展点Command。你可以写一个类实现CommandT接口然后在managementService.executeCommand(...)里执行。这个机制能让你在最底层直接操作引擎的数据和命令上下文。比如我想批量更新某个流程实例的业务状态字段又不希望走引擎暴露的 Service API就可以这样public class UpdateBizStatusCmd implements CommandVoid { private final String processInstanceId; private final String bizStatus; public UpdateBizStatusCmd(String processInstanceId, String bizStatus) { this.processInstanceId processInstanceId; this.bizStatus bizStatus; } Override public Void execute(CommandContext commandContext) { // 直接获取 Execution 实体管理器 ExecutionEntityManager executionEntityManager commandContext.getExecutionEntityManager(); ExecutionEntity executionEntity executionEntityManager.findById(processInstanceId); if (executionEntity ! null) { executionEntity.setBusinessStatus(bizStatus); // 这里会触发更新 SQLUPDATE ACT_RU_EXECUTION SET BUSINESS_STATUS_ ? WHERE ID_ ? } return null; } }执行方式managementService.executeCommand(new UpdateBizStatusCmd(2501, APPROVED));这种方式的好处是你完全绕开了高层的 Service API直接操作实体管理器效率最高。而且executeCommand内部会开启一个命令上下文事务边界清晰。但要注意自定义 Command 里必须使用CommandContext提供的实体管理器来操作实体不要自己 new 一个实体再调用 update否则引擎不知道这个实体是“受管状态”不会帮你生成更新 SQL。3.4 数据库表与实体管理器的对应关系Flowable 将每一张表都对应了一个实体管理器EntityManager统一注册在CommandContext里。常用几个实体管理器对应的表主要操作ExecutionEntityManagerACT_RU_EXECUTION流程实例、执行实例的增删改查TaskEntityManagerACT_RU_TASK待办任务的创建、完成、删除VariableInstanceEntityManagerACT_RU_VARIABLE流程变量的存取HistoricActivityInstanceEntityManagerACT_HI_ACTINST历史活动实例的记录IdentityLinkEntityManagerACT_RU_IDENTITYLINK候选人、候选组的关联关系当你调用runtimeService.startProcessInstanceByKey()时内部实际上会通过RepositoryService获取流程定义创建ExecutionEntity并调用ExecutionEntityManager.insert()根据 BPMN 模型创建初始的ACT_RU_TASK调用TaskEntityManager.insert()若有初始变量调用VariableInstanceEntityManager.insert()。所有这些 insert 操作都会先进入DbSqlSession的缓存区域在命令上下文提交时统一 flush 到数据库。提示DbSqlSession的 flush 是 Flowable 底层最核心的写操作。如果你在一个命令里做了大量实体变更最后 flush 时会按顺序执行 insert/update/delete。如果其中某一条 SQL 失败整个事务回滚。所以你在自定义命令里尽量避免批量查出来后循环修改实体尽量用批量 SQL减少 flush 的压力。4. 常见问题与排查技巧实录4.1 运行一段时间后任务表越来越大先明确一点ACT_RU_TASK和ACT_RU_EXECUTION理论上只保留“未完成”的数据。如果你发现这两张表的数据量异常膨胀基本可以断定有不少流程实例没有正常走完。常见原因有流程卡在等待节点比如收到消息后一直没有触发定时器任务一直重试失败某条分支执行实例异常没有正常 delete。排查思路查ACT_RU_EXECUTION数量SELECT PROC_DEF_ID_, COUNT(*) FROM ACT_RU_EXECUTION GROUP BY PROC_DEF_ID_;查ACT_RU_TASK上长期未办理的任务SELECT * FROM ACT_RU_TASK WHERE CREATE_TIME_ lt; DATE_SUB(NOW(), INTERVAL 7 DAY);检查是否有 job 一直失败SELECT * FROM ACT_RU_JOB WHERE RETRIES_ 0;处理策略通常是定时清理孤儿数据 对长期挂起的流程做人工干预或超时自动关闭。生产环境建议写一个定时任务扫描长期未结束的流程实例超过阈值就调用runtimeService.deleteProcessInstance()避免运行时表无限膨胀。4.2 分页查询很慢Flowable 的任务查询接口在数据量大时容易慢尤其是带taskCandidateUser条件时。因为引擎会 joinACT_RU_IDENTITYLINK表来过滤候选人这张表如果没有索引查询会变成全表扫描。建议做两件事第一给高频查询字段建索引。最常用的组合是CREATE INDEX idx_ru_task_procinst ON ACT_RU_TASK(PROC_INST_ID_); CREATE INDEX idx_ru_task_create_time ON ACT_RU_TASK(CREATE_TIME_); CREATE INDEX idx_ru_identitylink_task ON ACT_RU_IDENTITYLINK(TASK_ID_); CREATE INDEX idx_ru_execution_procdef ON ACT_RU_EXECUTION(PROC_DEF_ID_);第二如果你只需要当前用户名下的待办尽量用taskCandidateOrAssigned(String userId)它会在一个 SQL 里同时处理候选人、候选组和直接办理人避免多次查询再内存合并。Flowable 从 5.x 到 6.x分页方言是自动识别的MySQL 下最终会拼出LIMIT ? , ?。如果你发现分页查询很慢先在 MySQL 的慢查询日志里拿到真实 SQL然后看 explain 结果通常问题就是缺索引。4.3 历史表数据增长过快ACT_HI_*表是只增不改的流程跑久了必然膨胀。Flowable 官方提供了历史数据清理的 API在 6.x 里面可以配置spring: flowable: history-cleanup: enabled: true clean-after-days: 30不过生产环境我更推荐自己写清理任务。原因是 Flowable 自带清理逻辑对数据量大的表删除速度一般而且有可能锁表。自己清理时要先删子表再删主表顺序不能乱。基本顺序是删除ACT_HI_VARINST删除ACT_HI_ACTINST删除ACT_HI_TASKINST注意它有关联的 identity link 要先删最后删ACT_HI_PROCINST。DELETE FROM ACT_HI_VARINST WHERE PROC_INST_ID_ IN (SELECT ID_ FROM ACT_HI_PROCINST WHERE END_TIME_ lt; DATE_SUB(NOW(), INTERVAL 90 DAY)); -- 同理继续处理其它表而且我建议按照PROC_INST_ID_分批次删除每次删 5000 条避免一次删太多导致数据库锁和 binlog 暴涨。4.4 命令上下文事务边界问题很多人在自定义 Command 或者监听器里写业务逻辑时容易产生“中间状态不可见”的困惑。举个例子在任务监听器里调用runtimeService.setVariable()后你马上在同一个监听器里查数据库会发现查不到刚更新的值。因为 Flowable 的写操作并没有实时落库而是先更新了内存中的实体再在事务提交时统一 flush。如果你确实需要在事务提交前查到最新值应该复用一个CommandContextpublic void notify(DelegateTask delegateTask) { String processInstanceId delegateTask.getProcessInstanceId(); // 通过 CommandContext 获取到的是内存中的最新值 Object value Context.getCommandContext() .getVariableInstanceEntityManager() .findVariableInstanceByExecutionIdAndName(processInstanceId, approveStatus); System.out.println(value); }关键点不要通过 JdbcTemplate 或 MyBatis 的独立 SQL 去查 Flowable 的运行时表来判断流程状态因为在同一个事务里你可能会查到旧值或者脏数据。要查就通过 CommandContext 的实体管理器要确认最终值就等事务提交后再查。4.5 Flowable 自增主键与 MySQL 主键冲突Flowable 早期版本的主键生成是用自增 ID后来默认改成了 UUID 字符串。现在ACT_RU_EXECUTION.ID_、ACT_RU_TASK.ID_等都是 64 位的字符串用IdGenerator接口生成。如果你自定义了主键生成器一定要保证全局唯一否则插入时会出现主键冲突。需要改主键生成策略时通过配置类注入Bean public ProcessEngineConfigurationConfigurer processEngineConfigurationConfigurer() { return config - config.setIdGenerator(new CustomIdGenerator()); }不建议乱改除非你有明确的跨库合并或者分库分表需求。4.6 DB 连接池配置Flowable 对数据库连接的消耗比较大特别是大并发审批时。Spring Boot 默认的 HikariCP 连接池需要合理配置。我常用的配置spring: datasource: hikari: maximum-pool-size: 50 minimum-idle: 10 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000maximum-pool-size不是你随便拍脑袋定的。一个粗略算法核心业务高峰期并发审批任务数 × 每个任务处理平均耗时秒再除以 60得到大概需要的连接数。比如高峰 200 个并发请求每个请求平均 0.6 秒处理完那大约需要200 * 0.6 / 60 2个连接实际当然还要算上流程引擎内部的各种查询和 job 执行建议在这个估算基础上乘 5 到 10 倍作为初始值再压测调优。4.7 不同数据库之间的迁移注意点Flowable 支持多数据库但如果你要从 MySQL 迁到 PostgreSQL 或 Oracle有几个隐藏坑MySQL 的ACT_GE_BYTEARRAY中存的 BPMN XML 用的是LONGBLOB而 Oracle 里是BLOB迁移时要注意字节序。MySQL 的表名和列名默认不区分大小写但 Oracle 默认是大写。如果你在自定义 SQL 里写死了ACT_RU_TASK在 Oracle 没问题但在 PostgreSQL 里可能因为大小写问题报错。日期处理MySQL 的DATETIME和 Oracle 的TIMESTAMP精度不一样Flowable 内部有做转换但在你写原生 SQL 查询历史数据时要注意时间格式差异。5. 性能调优与常见扩展实践5.1 从日志里定位慢 SQL在 Spring Boot 里打开 Flowable 的 SQL 日志logging: level: org.flowable.engine.impl.db: DEBUG你会看到类似这样的输出 Preparing: select * from ACT_RU_TASK where ID_ ? Parameters: 2501(String) Columns: ID_, REV_, ... Row: 2501, 1, ...生产环境不要全开 DEBUG建议按需临时打开定位完问题就关掉。更好的做法是接一个数据库慢查询监控把执行时间超过 500ms 的 SQL 捞出来分析。5.2 通过CommandInterceptor做全局 DB 访问监控Flowable 允许你自定义拦截器链在命令执行前后埋点。比如我想统计所有命令的耗时和 SQL 条数public class SqlCountInterceptor extends AbstractCommandInterceptor { Override public T T execute(CommandConfig config, CommandT command) { long start System.currentTimeMillis(); try { return next.execute(config, command); } finally { long cost System.currentTimeMillis() - start; if (cost 1000) { log.warn(Command {} took {} ms, command.getClass().getSimpleName(), cost); } } } }然后注入到配置config.setCustomPreCommandInterceptors(Collections.singletonList(new SqlCountInterceptor()));这样你就能精准感知到每一次流程操作对数据库的压力分布。在实际项目中我靠这个方式抓到过不少因为在监听器里写了慢查询而拖垮整个事务的问题。5.3 批量操作与批量插入Flowable 6.x 提供了runtimeService.createProcessInstanceBuilder()等批量 API但底层对子流程、多实例的插入仍然是逐条执行。如果一次性启动大量流程实例比如批量审批 1000 条工单建议使用executorService分批并发启动控制每批大小每一批单独一个事务避免大事务导致锁竞争和 binlog 积压大批量流程启动时先关闭async-executor-activate等数据落完再开启 job 执行。5.4 在业务表与 Flowable 表之间维护关联不少团队会把 Flowable 的PROC_INST_ID_存在自己的业务表里查询时先查业务表再关联 Flowable 的表。这里有个最佳实践不要在业务 SQL 里 join Flowable 的运行时表。原因很简单运行时表数据是动态变化的join 会让你的业务查询绑定到 Flowable 的表结构上升级引擎版本时很容易出问题。更稳妥的做法是业务表只存PROC_INST_ID_和TASK_ID_查询任务状态时调用 Flowable API然后把需要冗余的状态字段回写到业务表。这样业务查询完全不依赖 Flowable 的数据库结构将来换引擎或者调整表结构时影响最小。6. 从源码角度看 DB 操作的执行链路想彻底搞懂 Flowable 的 DB 访问还是得顺着源码走一遍。从你调用taskService.complete(taskId)开始核心链路是这样的TaskServiceImpl.complete()进入CommandExecutor。CommandExecutor经过拦截器链日志、事务、缓存等最终执行CompleteTaskCmd。CompleteTaskCmd.execute()从CommandContext里获取TaskEntityManager先根据taskId查任务实体。引擎根据 BPMN 模型走到下一个节点可能创建新的任务、更新执行实例状态、写入历史活动记录。所有实体变更暂存在DbSqlSession的缓存中。命令执行完后拦截器链最外层的SpringTransactionInterceptor触发事务提交。DbSqlSession.flush()把所有待更新的实体映射成一条条 SQL按顺序发送给数据库。这套机制的精髓在于API 层的每次调用最终在 DB 层体现为“一个事务内多条 SQL”事务边界在命令Command层面而不是在每个实体操作层面。这也是为什么 Flowable 在高并发下能保证数据一致性。如果你在源码里打断点看启动流程的过程会发现真正执行 insert 的时机并不是runtimeService.startProcessInstanceByKey()这一行代码而是在你方法返回之前事务拦截器提交的时候。这是很多人在调试时感到困惑的地方。6.1DbSqlSession的实现细节DbSqlSession是 Flowable 数据库访问的核心枢纽。它维护了两个缓存区insertedObjects存放待插入的实体deletedObjects存放待删除的实体cachedObjects存放已经查出来、并且可能被修改的实体flush 时会按顺序执行删除操作先删子表再删主表避免外键约束问题插入操作更新操作所有实体只要能识别出主键变更就执行 update。每个实体都实现了Entity接口里面有个Revision字段REV_列每次更新都会把版本号加一MyBatis 的updateSQL 会带上where REV_ ?防止并发下的脏写。这就是 Flowable 乐观锁的实现方式。6.2 历史数据异步写入的实现Flowable 从 6.x 开始支持历史数据的异步写入通过asyncHistoryExecutor实现。开启后ACT_HI_*表的写入不会和运行时表在同一个事务里而是通过 Job 异步处理。适合历史数据量极大、又不想拖慢主流程性能的场景。spring: flowable: async-history-executor-activate: true但开了异步历史也有代价稍等几秒后你才能查到历史记录流程列表页如果依赖历史表做统计可能短暂出现数据延迟。所以要不要开得看你的业务是对实时性敏感还是对主流程吞吐更敏感。6.3 自定义 SQL 直接查询 Flowable 表有些小伙伴喜欢直接用 MyBatis 写原生 SQL 查 Flowable 的表这没问题但要注意不要直接修改ACT_RU_*表因为引擎有自己的缓存你改了库里的数据引擎内存中还是旧值后续 flush 可能把你的修改覆盖掉。查询历史表可以用原生 SQL但表名、字段名尽量从ProcessEngineConfiguration的tableName常量里引用方便以后换库兼容。我自己在项目里就踩过这种坑运维同学嫌查询慢直接往ACT_RU_TASK加了个索引这个其实没事但又有一次他为了修正数据直接 update 了ACT_RU_TASK.ASSIGNEE_结果引擎缓存里还是旧办理人后续任务操作直接覆盖了他的修改白改一遍还产生了脏数据。所以运行时表的数据修改一定要走 API不能绕过引擎。7. 我在实际项目中积累的几个心得做了这么多年 Flowable 二开真正让我觉得“DB 访问这块值得好好研究”的基本都是线上事故或者性能问题逼出来的。第一个心得是永远不要在你的业务事务里混合操作 Flowable 和业务表。很多人图省事在自己 Service 方法上标Transactional然后既改业务表又调用 Flowable API。一旦 Flowable 内部报错你的业务事务也被标记 rollback-only最后抛出的异常你可能都看不懂。建议的方式是分离事务边界Flowable 操作一个事务业务表操作另一个事务通过事件或消息做最终一致性。第二个心得是监控表数据量要形成日常习惯。我们每周都会跑一个 SQL 统计各ACT_RU_*表和ACT_HI_*表的增长速率超过阈值就提醒。等出了问题再去看表往往已经晚了。第三个心得是升级 Flowable 版本前一定要看表结构变更日志。Flowable 的大版本升级经常会改表结构、加索引、调整字段长度直接在生产环境换 jar 包不跑迁移脚本基本都会挂。我一般会在测试环境先跑一次老的流程数据做回归重点看历史查询和统计功能。8. 最后再分享一个小技巧Flowable 官方提供的 REST API 可以直接操作流程但如果你是在高并发、大数据量的场景下使用建议别有请求就打 REST。内部 REST API 的每次请求都会新开一个命令上下文频繁调用会产生大量 DB 交互和事务开销。更好的做法是后端封装一层批量接口比如“批量审批 100 条任务”在一个事务里循环调用taskService.complete()而不是让前端发 100 个请求。批量处理时注意把对数据库的写操作控制在合理批次内必要时手动调大maximumPoolSize并调小事务粒度。我碰到过一次性审批 5000 条记录的场景当时直接把数据库连接池打满导致整个服务端其他查询全部阻塞后来改成每批 500 条、每批单独事务才算稳定下来。Flowable 的 DB 访问层是整个引擎最结实也最关键的部分理解它的表设计、缓存机制、事务边界和 SQL 执行链路你在做二次开发、性能调优、问题排查时都会顺手很多。希望这篇梳理能帮正在啃 Flowable 源码的你把这块骨头啃下来。
阅读完成 · 觉得有帮助?
咨询建站