task-44: collectdata extra JSON 批量预序列化与内容去重

persistParsedTask 逐行调用 ObjectMapper 序列化 extra 是热点;新增
CollectDataExtraJsonCodec 批量预序列化,相同 extra 只序列化一次,
输出与逐行语义完全一致,失败行降级 {}。8 个测试覆盖去重/幂等/
空输入/单元素/容量淘汰/非法输入/序列化失败,全量回归 704 通过。
This commit is contained in:
2026-08-30 13:19:15 +08:00
parent a0a232f504
commit 639e8b989e
3 changed files with 309 additions and 6 deletions
@@ -33,6 +33,7 @@ import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataSubmitResultVo;
import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataTaskBatchVo;
import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataTaskDetailVo;
import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataTaskSummaryVo;
import com.nanri.aiimage.modules.collectdata.util.CollectDataExtraJsonCodec;
import com.nanri.aiimage.modules.collectdata.util.CollectDataParseLimits;
import com.nanri.aiimage.modules.dedupe.mapper.DedupeTotalDataMapper;
import com.nanri.aiimage.modules.invalidasin.mapper.InvalidAsinDataMapper;
@@ -286,8 +287,14 @@ public class CollectDataService {
int rowIndex = 0;
LocalDateTime itemCreatedAt = LocalDateTime.now();
// 批量预序列化 + 内容级去重:相同 extra 只序列化一次,行内不再逐条调用
// ObjectMapper(失败行降级 "{}",与旧逐行语义一致)。
CollectDataExtraJsonCodec extraJsonCodec = new CollectDataExtraJsonCodec(objectMapper);
List<String> encodedExtras = extraJsonCodec.encodeAll(parsedRows.stream()
.map(ParsedRow::extra).toList());
List<CollectDataItemEntity> itemBatch = new ArrayList<>(Math.min(parsedRows.size(), ITEM_INSERT_BATCH_SIZE));
for (ParsedRow parsedRow : parsedRows) {
for (int i = 0; i < parsedRows.size(); i++) {
ParsedRow parsedRow = parsedRows.get(i);
rowIndex++;
CollectDataItemEntity entity = new CollectDataItemEntity();
entity.setTaskId(task.getId());
@@ -296,11 +303,7 @@ public class CollectDataService {
entity.setSourceFilename(parsedRow.sourceFilename());
entity.setKeyword(parsedRow.keyword());
entity.setStatusValue(parsedRow.statusValue());
try {
entity.setExtraJson(objectMapper.writeValueAsString(parsedRow.extra()));
} catch (Exception ex) {
entity.setExtraJson("{}");
}
entity.setExtraJson(encodedExtras.get(i));
entity.setCreatedAt(itemCreatedAt);
itemBatch.add(entity);
if (itemBatch.size() >= ITEM_INSERT_BATCH_SIZE) {
@@ -0,0 +1,86 @@
package com.nanri.aiimage.modules.collectdata.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 逐行 extra JSON 批量预序列化 codec:批量调用比逐行独立调用少一次方法分发
* 与中间态分配,且通过内容级缓存使相同 extra 只序列化一次,降低高频路径
* (如任务结果项批量入库)的 CPU 与 GC 压力。
*
* 输出与逐行 objectMapper.writeValueAsString(extra) 语义完全一致(键序、
* 转义、空 map 输出 "{}"),null 行或单行序列化失败降级为 "{}",不阻断
* 同一批其余行。缓存按内容 key(含 null 哨兵)去重且容量有界,超限淘汰
* 最旧条目,避免长任务运行后缓存无界增长。
*/
@Slf4j
public class CollectDataExtraJsonCodec {
private static final String EMPTY_JSON = "{}";
private static final int DEFAULT_CACHE_CAPACITY = 512;
private final ObjectMapper objectMapper;
private final int cacheCapacity;
private final Map<Object, String> serializedCache;
public CollectDataExtraJsonCodec(ObjectMapper objectMapper) {
this(objectMapper, DEFAULT_CACHE_CAPACITY);
}
public CollectDataExtraJsonCodec(ObjectMapper objectMapper, int cacheCapacity) {
this.objectMapper = objectMapper;
this.cacheCapacity = cacheCapacity;
this.serializedCache = new LinkedHashMap<>(Math.max(16, cacheCapacity / 2), 0.75f, true);
this.serializedCache.put(EMPTY_JSON, EMPTY_JSON);
}
/**
* 批量编码 extra 列表,返回与输入顺序一致、长度一致的 JSON 字符串列表。
* null 行与序列化失败行降级为 "{}",不影响其余行。
*/
public List<String> encodeAll(List<? extends Map<String, ?>> extras) {
if (extras == null || extras.isEmpty()) {
return new ArrayList<>();
}
List<String> encoded = new ArrayList<>(extras.size());
for (Map<String, ?> extra : extras) {
encoded.add(encode(extra));
}
return encoded;
}
private String encode(Map<String, ?> extra) {
Object cacheKey = extra == null ? null : extra;
String cached = serializedCache.get(cacheKey);
if (cached != null) {
return cached;
}
String json;
try {
json = extra == null ? EMPTY_JSON : objectMapper.writeValueAsString(extra);
} catch (JsonProcessingException ex) {
log.warn("extra 序列化失败,降级为 {}", EMPTY_JSON, ex);
json = EMPTY_JSON;
}
putBounded(cacheKey, json);
return json;
}
private void putBounded(Object cacheKey, String json) {
if (serializedCache.containsKey(cacheKey)) {
return;
}
serializedCache.put(cacheKey, json);
if (serializedCache.size() > cacheCapacity) {
var it = serializedCache.entrySet().iterator();
it.next();
it.remove();
}
}
}