Skip to content

Commit a67197f

Browse files
xlorneclaude
andcommitted
perf: 消除保存待办时的 getByTodoKey N+1,改为批量 findByKeys 分块加载 issue #210
- FlowRecordSaveService.saveTodoMargeRecords 先批量加载已存在待办,替代循环内逐条 getByTodoKey(每 20 条待办 20 次 SELECT → 1 次 IN 查询) - 按 TODO_KEY_BATCH_SIZE=500 分块查询,避免海量 key 拼超大 IN 子句/结果集过大导致 OOM - FlowTodoRecordRepository 新增 findByKeys,实现于 mock/test/Infra 三层仓储 - 新增集成断言:批量场景逐条 getByTodoKey 调用数远小于待办数,且走批量 findByKeys - 全量 clean install BUILD SUCCESS,227 framework + example 真实 JPA 集成测试全绿 #210 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent c49e1a9 commit a67197f

7 files changed

Lines changed: 121 additions & 1 deletion

File tree

flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/FlowTodoRecordRepositoryMockImpl.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,21 @@
44
import com.codingapi.flow.repository.FlowTodoRecordRepository;
55

66
import java.util.HashMap;
7+
import java.util.HashSet;
78
import java.util.List;
89
import java.util.Map;
10+
import java.util.Set;
911

1012
public class FlowTodoRecordRepositoryMockImpl implements FlowTodoRecordRepository {
1113

1214
private final Map<Long, FlowTodoRecord> cache = new HashMap<>();
1315
private final Map<String, FlowTodoRecord> cacheByMageKey = new HashMap<>();
1416
private long nextId = 1;
1517

18+
// 查询计数器,供测试断言"按 key 逐条查询已被批量查询替代"(N+1 消除)
19+
private int getByTodoKeyCalls;
20+
private int findByKeysCalls;
21+
1622
@Override
1723
public void save(FlowTodoRecord record) {
1824
if (record.getId() > 0) {
@@ -41,9 +47,28 @@ public void delete(FlowTodoRecord margeRecord) {
4147

4248
@Override
4349
public FlowTodoRecord getByTodoKey(String key) {
50+
getByTodoKeyCalls++;
4451
return cacheByMageKey.get(key);
4552
}
4653

54+
@Override
55+
public List<FlowTodoRecord> findByKeys(List<String> keys) {
56+
findByKeysCalls++;
57+
Set<String> keySet = new HashSet<>(keys);
58+
return cacheByMageKey.entrySet().stream()
59+
.filter(entry -> keySet.contains(entry.getKey()))
60+
.map(Map.Entry::getValue)
61+
.toList();
62+
}
63+
64+
public int getGetByTodoKeyCalls() {
65+
return getByTodoKeyCalls;
66+
}
67+
68+
public int getFindByKeysCalls() {
69+
return findByKeysCalls;
70+
}
71+
4772
public List<FlowTodoRecord> findByOperatorId(long operatorId) {
4873
return cache.values().stream().filter(record -> record.getCurrentOperatorId() == operatorId).toList();
4974
}

flow-engine-framework/src/main/java/com/codingapi/flow/repository/FlowTodoRecordRepository.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ public interface FlowTodoRecordRepository {
1010

1111
FlowTodoRecord getByTodoKey(String key);
1212

13+
/**
14+
* 按多个待办合并 key 批量加载已存在的待办记录,用于替代循环内逐条 {@link #getByTodoKey(String)} 的 N+1 查询。
15+
*
16+
* @param keys 待办合并 key 列表
17+
* @return 已存在的待办记录
18+
*/
19+
List<FlowTodoRecord> findByKeys(List<String> keys);
20+
1321
void delete(FlowTodoRecord margeRecord);
1422

1523
void save(FlowTodoRecord margeRecord);

flow-engine-framework/src/main/java/com/codingapi/flow/service/FlowRecordSaveService.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,21 @@
88
import com.codingapi.flow.repository.FlowTodoRecordRepository;
99

1010
import java.util.ArrayList;
11+
import java.util.HashMap;
1112
import java.util.List;
13+
import java.util.Map;
14+
import java.util.Objects;
1215

1316
/**
1417
* 流程记录保存服务,负责保存流程记录和待办记录的合并关系
1518
*/
1619
class FlowRecordSaveService {
1720

21+
/**
22+
* 批量加载已存在待办时单批查询的 key 上限,避免单个 IN 子句过大(SQL 限制)与结果集过大(内存)。
23+
*/
24+
private static final int TODO_KEY_BATCH_SIZE = 500;
25+
1826
private final List<FlowRecord> flowRecords;
1927

2028
private FlowTodoRecordRepository flowTodoRecordRepository;
@@ -41,12 +49,29 @@ public void registerRepositories(FlowTodoRecordRepository flowTodoRecordReposito
4149

4250

4351
private void saveTodoMargeRecords() {
52+
// 批量加载已存在的待办(而非逐条 getByTodoKey 的 N+1):
53+
// 按分块大小分批 findByKeys,避免海量 key 拼在单个 IN 子句里导致 SQL/结果集过大(OOM 风险)。
54+
List<String> todoKeys = flowRecords.stream()
55+
.filter(FlowRecord::isTodo)
56+
.map(FlowRecord::getTodoKey)
57+
.filter(Objects::nonNull)
58+
.distinct()
59+
.toList();
60+
Map<String, FlowTodoRecord> existedByKey = new HashMap<>();
61+
for (int start = 0; start < todoKeys.size(); start += TODO_KEY_BATCH_SIZE) {
62+
List<String> chunk = todoKeys.subList(start, Math.min(start + TODO_KEY_BATCH_SIZE, todoKeys.size()));
63+
for (FlowTodoRecord existed : flowTodoRecordRepository.findByKeys(chunk)) {
64+
existedByKey.put(existed.getTodoKey(), existed);
65+
}
66+
}
67+
4468
List<FlowTodoRecord> flowTodoRecords = new ArrayList<>();
4569
for (FlowRecord flowRecord : flowRecords) {
4670
if (flowRecord.isTodo()) {
47-
FlowTodoRecord todoMargeRecord = flowTodoRecordRepository.getByTodoKey(flowRecord.getTodoKey());
71+
FlowTodoRecord todoMargeRecord = existedByKey.get(flowRecord.getTodoKey());
4872
if (todoMargeRecord == null) {
4973
todoMargeRecord = new FlowTodoRecord(flowRecord);
74+
existedByKey.put(flowRecord.getTodoKey(), todoMargeRecord);
5075
} else {
5176
todoMargeRecord.update(flowRecord);
5277
if (flowRecord.isMergeable()) {

flow-engine-framework/src/test/java/com/codingapi/flow/repository/FlowTodoRecordRepositoryImpl.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@
33
import com.codingapi.flow.record.FlowTodoRecord;
44

55
import java.util.HashMap;
6+
import java.util.HashSet;
67
import java.util.List;
78
import java.util.Map;
9+
import java.util.Set;
810

911
public class FlowTodoRecordRepositoryImpl implements FlowTodoRecordRepository {
1012

1113
private final Map<Long, FlowTodoRecord> cache = new HashMap<>();
1214
private final Map<String, FlowTodoRecord> cacheByMageKey = new HashMap<>();
1315
private long nextId = 1;
1416

17+
// 查询计数器,供测试断言"按 key 逐条查询已被批量查询替代"(N+1 消除)
18+
private int getByTodoKeyCalls;
19+
private int findByKeysCalls;
20+
1521
@Override
1622
public void save(FlowTodoRecord record) {
1723
if (record.getId() > 0) {
@@ -40,9 +46,28 @@ public void delete(FlowTodoRecord margeRecord) {
4046

4147
@Override
4248
public FlowTodoRecord getByTodoKey(String key) {
49+
getByTodoKeyCalls++;
4350
return cacheByMageKey.get(key);
4451
}
4552

53+
@Override
54+
public List<FlowTodoRecord> findByKeys(List<String> keys) {
55+
findByKeysCalls++;
56+
Set<String> keySet = new HashSet<>(keys);
57+
return cacheByMageKey.entrySet().stream()
58+
.filter(entry -> keySet.contains(entry.getKey()))
59+
.map(Map.Entry::getValue)
60+
.toList();
61+
}
62+
63+
public int getGetByTodoKeyCalls() {
64+
return getByTodoKeyCalls;
65+
}
66+
67+
public int getFindByKeysCalls() {
68+
return findByKeysCalls;
69+
}
70+
4671
public List<FlowTodoRecord> findByOperatorId(long operatorId) {
4772
return cache.values().stream().filter(record -> record.getCurrentOperatorId() == operatorId).toList();
4873
}

flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowIssue210SubProcessPerformanceTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
import static org.junit.jupiter.api.Assertions.assertEquals;
4141
import static org.junit.jupiter.api.Assertions.assertNotNull;
42+
import static org.junit.jupiter.api.Assertions.assertTrue;
4243

4344
/**
4445
* 问题 210 复现测试:主流程 C(子流程节点)一次性创建 6 个子流程、每个子流程 B 节点 20 个审批人。
@@ -127,6 +128,30 @@ void shouldMeasureSubProcessBulkCreationTimeWithGatewayDelay() {
127128
+ " (网关延时 " + GATEWAY_DELAY_MS + "ms/次)");
128129
}
129130

131+
/**
132+
* 测试目标:验证保存待办时不再逐条 getByTodoKey(N+1),而是改用批量 findByKeys。
133+
* 前置条件:网关不带延时(本类每次测试都新建 factory),强制执行 bulk 场景。
134+
* 执行步骤:完成与 {@code shouldMeasureSubProcessBulkCreationTimeWithGatewayDelay} 相同的
135+
* 6 子流程 × 20 审批人(120 条待办)创建。
136+
* 期望断言:逐条 getByTodoKey 的调用次数远小于待办数量(仅来自节点完成清理路径),
137+
* 且调用过批量 findByKeys。
138+
*/
139+
@Test
140+
void shouldBatchLoadExistingTodosInsteadOfPerRecordGet() {
141+
Map<String, Object> data = new LinkedHashMap<>();
142+
data.put("content", "parent");
143+
data.put("approvers", approvers.stream().map(User::getUserId).toList());
144+
long parentRecordId = createParent(data);
145+
approveMain(factory.flowRecordRepository.get(parentRecordId), parentStart, data);
146+
FlowRecord parentBRecord = findTodo(initiator, parentB.getId());
147+
approveMain(parentBRecord, parentB, data);
148+
149+
assertTrue(factory.flowTodoRecordRepository.getGetByTodoKeyCalls() < APPROVER_COUNT,
150+
"批量创建待办时应以批量 findByKeys 替代逐条 getByTodoKey,避免 N+1");
151+
assertTrue(factory.flowTodoRecordRepository.getFindByKeysCalls() >= 1,
152+
"应调用批量 findByKeys 加载已存在待办");
153+
}
154+
130155
// ---------- 网关延时 + 计数 ----------
131156

132157
private static class DelayedGateway implements FlowOperatorGateway {

flow-engine-starter-infra/src/main/java/com/codingapi/flow/infra/jpa/FlowTodoRecordEntityRepository.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@
66
import org.springframework.data.domain.PageRequest;
77
import org.springframework.data.jpa.repository.Query;
88

9+
import java.util.List;
10+
911
public interface FlowTodoRecordEntityRepository extends FastRepository<FlowTodoRecordEntity,Long> {
1012

1113
FlowTodoRecordEntity getByTodoKey(String todoKey);
1214

15+
@Query("from FlowTodoRecordEntity r where r.todoKey in ?1")
16+
List<FlowTodoRecordEntity> findByKeys(List<String> keys);
17+
1318

1419
@Query("from FlowTodoRecordEntity r where r.currentOperatorId = ?1")
1520
Page<FlowTodoRecordEntity> findTodoRecordPage(long currentOperatorId, PageRequest pageRequest);

flow-engine-starter-infra/src/main/java/com/codingapi/flow/infra/repository/impl/FlowTodoRecordRepositoryImpl.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ public FlowTodoRecord getByTodoKey(String key) {
2626
return FlowTodoRecordConvertor.convert(flowTodoRecordEntityRepository.getByTodoKey(key));
2727
}
2828

29+
@Override
30+
public List<FlowTodoRecord> findByKeys(List<String> keys) {
31+
return flowTodoRecordEntityRepository.findByKeys(keys).stream()
32+
.map(FlowTodoRecordConvertor::convert)
33+
.toList();
34+
}
35+
2936
@Override
3037
public void delete(FlowTodoRecord margeRecord) {
3138
flowTodoRecordEntityRepository.deleteById(margeRecord.getId());

0 commit comments

Comments
 (0)