场景说明
主流程节点 A-B-C-D-E,其中 C 节点为子流程节点。A 为开始节点,E 为结束节点,B 和 D 均为审批节点。
子流程节点 A-B-C-D,A 为开始节点,D 为结束节点,B 和 C 均为审批节点。
主流程 C 节点发起 6 个子流程(配置为"发起并提交"),每个子流程的 B 节点并签 20 个审批人(通过表单 approvers 字段传入),即一次性创建 6 × 20 = 120 条流程记录。
为模拟真实环境,在流程操作人网关查询用户时增加 5ms 延时,检测主流程 C 节点提交的总体耗时,并定位优化空间。
本地复现与量化(单元测试)
新增复现测试 FlowIssue210SubProcessPerformanceTest:内存 mock 仓储 + 网关 5ms 延时,测得:
| 指标 |
数值 |
网关批量查询 findByIds |
6 次(覆盖 120 人次) |
网关单点查询 get() |
15 次 |
| 网关总查询 |
21 次 ≈ 105ms |
| B/C 节点提交总耗时 |
≈ 204ms |
结论:本地 mock 仓储下,操作人网关(~100ms)约占一半,但这不是真实环境的主体。
真正慢的本质:不是网关,而是大量串行的 DB 往返
线上环境差异:数据库是达梦、走内网,本地单元测试用的是内存 mock 仓储(0 DB 往返)。监控显示操作人查询仅 ~5ms/次、共 ~100ms,但整体耗时约 10 秒,说明真正的瓶颈在记录持久化而非网关。
一次 C 节点批量提交(同一 @Transactional 内串行),粗算 SQL 往返:
| 操作 |
类型/数量 |
| 6 个子流程开始记录 |
INSERT × 6 |
| 6 × 20 条 B 节点并签审批记录 |
INSERT × 120 |
每个子流程 B 待办逐条查库 getByTodoKey |
N+1 SELECT × 120 |
待办记录 t_flow_todo_record |
INSERT × 120 |
并签合并 t_flow_todo_marge |
INSERT × 120 |
合计 ~500 条独立 SQL 往返,因受两点放大:
- 主键
GenerationType.IDENTITY 使 Hibernate 无法 JDBC 批插(每条 INSERT 一次往返);
saveTodoMargeRecords 逐条 getByTodoKey 的 N+1。
达梦 + 内网 + 每往返 15~25ms → ~500 × 20ms ≈ 10s,与线上反馈吻合。
优化实施(已合入 dev)
1. 高频热表主键 IDENTITY → SEQUENCE(原理:解锁 Hibernate 批插)
仅改 4 张高频写热表,低频流程设计表(t_flow_workflow/t_flow_workflow_version/t_flow_workflow_runtime)保持 IDENTITY 不动:
t_flow_record、t_flow_todo_record、t_flow_todo_marge、t_flow_sub_process_record
- 示例配置开启
hibernate.jdbc.batch_size=50 + order_inserts + order_updates
- 运行机制受影响面为零(
id>0 判定、setId(entity.getId())、fromId 引用、按 id 排序语义均不变)
2. 消除 N+1 ✔
FlowRecordSaveService.saveTodoMargeRecords 改为先分块批量加载已存在待办再循环处理:
- 新增
FlowTodoRecordRepository.findByKeys(...),按 TODO_KEY_BATCH_SIZE=500 分块,避免海量 key 拼超大 IN 子句/结果集过大导致 OOM
- N+1(每 20 条 20 次 SELECT)→ 每块 1 次 IN 查询
- 新增断言验证:批量场景下逐条
getByTodoKey 调用数远小于待办数
(removeTodoMergeRecords 对已办记录的逐条 getByTodoKey 清理路径因量级较小暂未改,可后续按需处理。)
遗留(需运维侧手动执行)
达梦存量库迁移:ddl-auto=update 只新建序列对象,需手动对上述 4 张热表:
- 去掉存量列的 IDENTITY 属性;
- 建序列并把起始值播种到
MAX(id)+1(否则新插入主键冲突)。
验证
- 全项目
./mvnw clean install BUILD SUCCESS
- framework 227 个测试通过
- example 3 个
@SpringBootTest 集成测试(真实 JPA + H2)验证 SEQUENCE 与 findByKeys 实际生效
场景说明
主流程节点 A-B-C-D-E,其中 C 节点为子流程节点。A 为开始节点,E 为结束节点,B 和 D 均为审批节点。
子流程节点 A-B-C-D,A 为开始节点,D 为结束节点,B 和 C 均为审批节点。
主流程 C 节点发起 6 个子流程(配置为"发起并提交"),每个子流程的 B 节点并签 20 个审批人(通过表单
approvers字段传入),即一次性创建 6 × 20 = 120 条流程记录。为模拟真实环境,在流程操作人网关查询用户时增加 5ms 延时,检测主流程 C 节点提交的总体耗时,并定位优化空间。
本地复现与量化(单元测试)
新增复现测试
FlowIssue210SubProcessPerformanceTest:内存 mock 仓储 + 网关 5ms 延时,测得:findByIdsget()结论:本地 mock 仓储下,操作人网关(~100ms)约占一半,但这不是真实环境的主体。
真正慢的本质:不是网关,而是大量串行的 DB 往返
线上环境差异:数据库是达梦、走内网,本地单元测试用的是内存 mock 仓储(0 DB 往返)。监控显示操作人查询仅 ~5ms/次、共 ~100ms,但整体耗时约 10 秒,说明真正的瓶颈在记录持久化而非网关。
一次 C 节点批量提交(同一
@Transactional内串行),粗算 SQL 往返:getByTodoKeyt_flow_todo_recordt_flow_todo_marge合计 ~500 条独立 SQL 往返,因受两点放大:
GenerationType.IDENTITY使 Hibernate 无法 JDBC 批插(每条 INSERT 一次往返);saveTodoMargeRecords逐条getByTodoKey的 N+1。达梦 + 内网 + 每往返 15~25ms →
~500 × 20ms ≈ 10s,与线上反馈吻合。优化实施(已合入 dev)
1. 高频热表主键 IDENTITY → SEQUENCE(原理:解锁 Hibernate 批插)
仅改 4 张高频写热表,低频流程设计表(
t_flow_workflow/t_flow_workflow_version/t_flow_workflow_runtime)保持IDENTITY不动:t_flow_record、t_flow_todo_record、t_flow_todo_marge、t_flow_sub_process_recordhibernate.jdbc.batch_size=50+order_inserts+order_updatesid>0判定、setId(entity.getId())、fromId引用、按 id 排序语义均不变)2. 消除 N+1 ✔
FlowRecordSaveService.saveTodoMargeRecords改为先分块批量加载已存在待办再循环处理:FlowTodoRecordRepository.findByKeys(...),按TODO_KEY_BATCH_SIZE=500分块,避免海量 key 拼超大 IN 子句/结果集过大导致 OOMgetByTodoKey调用数远小于待办数(
removeTodoMergeRecords对已办记录的逐条getByTodoKey清理路径因量级较小暂未改,可后续按需处理。)遗留(需运维侧手动执行)
达梦存量库迁移:
ddl-auto=update只新建序列对象,需手动对上述 4 张热表:MAX(id)+1(否则新插入主键冲突)。验证
./mvnw clean installBUILD SUCCESS@SpringBootTest集成测试(真实 JPA + H2)验证 SEQUENCE 与findByKeys实际生效