task-44: collectdata extra JSON 批量预序列化与内容去重
persistParsedTask 逐行调用 ObjectMapper 序列化 extra 是热点;新增
CollectDataExtraJsonCodec 批量预序列化,相同 extra 只序列化一次,
输出与逐行语义完全一致,失败行降级 {}。8 个测试覆盖去重/幂等/
空输入/单元素/容量淘汰/非法输入/序列化失败,全量回归 704 通过。
This commit is contained in:
+9
-6
@@ -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) {
|
||||
|
||||
+86
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user