task-51: accepted 行序列化与 hash 批量生成

CollectDataResultDetailCodec.encodeRefsWithHash 一次迭代为整 chunk 生成
全部行引用 JSON 及其 SHA-256(offset 从 startOffset 递增),带
MAX_REFS_PER_BATCH 上限与参数校验;CollectDataResultItemBatchWriter
接入批量生成,删除逐行 encodeRef+hash 循环,同 chunk 重提结果逐字节一致。
This commit is contained in:
2026-08-30 15:39:38 +08:00
parent 687bc18510
commit a29d6e6ce4
3 changed files with 227 additions and 4 deletions
@@ -6,6 +6,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataResultRowVo;
import org.springframework.stereotype.Component;
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
@@ -26,6 +29,13 @@ public class CollectDataResultDetailCodec {
public static final String REF_FIELD_OFFSET = "offset";
public static final String REF_FIELD_PAYLOAD = "payload";
/** 单次批量引用生成的上限:与结果明细 batch size 同量级,拒绝前不分配结果。 */
public static final int MAX_REFS_PER_BATCH = 100_000;
/** 一批引用及各自 hashrefJson 为行引用 JSONpayloadHash 为其 SHA-256。 */
public record RefWithHash(String refJson, String payloadHash) {
}
private final ObjectMapper objectMapper;
public CollectDataResultDetailCodec(ObjectMapper objectMapper) {
@@ -101,9 +111,50 @@ public class CollectDataResultDetailCodec {
}
}
/**
* 批量生成整 chunk 的引用 JSON 及其 SHA-256 hashoffset 从 startOffset
* 递增共 count 行,一次迭代完成序列化 + hash,替代调用方逐行两轮循环。
* 非法参数(负 chunk/负 startOffset/负 count/null 或空白指针)抛
* IllegalArgumentExceptioncount 超过 MAX_REFS_PER_BATCH 时在分配
* 结果前拒绝,不发生无界内存增长。
*/
public List<RefWithHash> encodeRefsWithHash(int chunkIndex, int startOffset, int count, String pointer) {
if (chunkIndex < 0 || startOffset < 0 || count < 0 || pointer == null || pointer.isBlank()) {
throw new IllegalArgumentException("invalid chunk detail ref batch chunk=" + chunkIndex
+ " startOffset=" + startOffset + " count=" + count + " pointer=" + pointer);
}
if (count > MAX_REFS_PER_BATCH) {
throw new IllegalArgumentException("chunk detail ref batch too large count=" + count
+ " max=" + MAX_REFS_PER_BATCH);
}
if (count == 0) {
return List.of();
}
List<RefWithHash> refs = new ArrayList<>(count);
for (int offset = startOffset; offset < startOffset + count; offset++) {
String refJson = encodeRef(chunkIndex, offset, pointer);
refs.add(new RefWithHash(refJson, sha256(refJson)));
}
return refs;
}
/** 结果明细行引用:chunk 序号 + 行内 offset + chunk 明细对象指针。 */
public record ChunkRef(@JsonProperty(REF_FIELD_CHUNK) int chunkIndex,
@JsonProperty(REF_FIELD_OFFSET) int offset,
@JsonProperty(REF_FIELD_PAYLOAD) String pointer) {
}
private static String sha256(String value) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] bytes = digest.digest((value == null ? "" : value).getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (Exception ex) {
throw new IllegalStateException("chunk detail ref hash failed", ex);
}
}
}
@@ -55,7 +55,7 @@ public class CollectDataResultItemBatchWriter {
if (rows == null || rows.isEmpty()) {
return new UpsertCounts(0, 0);
}
String scopeHash = hash(scopeKey);
String scopeHash = sha256(scopeKey);
// 一次性取回本 scope 现有行,构建 item_key → 现有行 映射(hash 相等即跳过)。
List<TaskResultItemEntity> existingList = taskResultItemMapper.selectList(
new LambdaQueryWrapper<TaskResultItemEntity>()
@@ -72,6 +72,9 @@ public class CollectDataResultItemBatchWriter {
List<TaskResultItemEntity> toUpsert = new ArrayList<>();
int skipped = 0;
LocalDateTime now = LocalDateTime.now();
// 批量生成整 chunk 的引用 JSON + hash(一次迭代),替代逐行 encodeRef + hash。
List<CollectDataResultDetailCodec.RefWithHash> refsWithHash =
resultDetailCodec.encodeRefsWithHash(chunkIndex, 0, rows.size(), storedDetail);
for (int offset = 0; offset < rows.size(); offset++) {
CollectDataResultRowVo row = rows.get(offset);
if (row == null || row.getAsin() == null || row.getAsin().isBlank()) {
@@ -80,8 +83,8 @@ public class CollectDataResultItemBatchWriter {
String itemKey = "asin:" + row.getAsin();
// chunk 级引用共享同一 RustFS 对象(deterministic key,同 chunk 重提
// 覆盖同一对象,引用 pointer 稳定,无需删除)。
String refJson = resultDetailCodec.encodeRef(chunkIndex, offset, storedDetail);
String payloadHash = hash(refJson);
String refJson = refsWithHash.get(offset).refJson();
String payloadHash = refsWithHash.get(offset).payloadHash();
TaskResultItemEntity existing = existingByKey.get(itemKey);
if (existing != null && Objects.equals(existing.getPayloadHash(), payloadHash)) {
skipped++;
@@ -124,7 +127,7 @@ public class CollectDataResultItemBatchWriter {
return new UpsertCounts(written, skipped);
}
private String hash(String value) {
private static String sha256(String value) {
try {
java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
byte[] bytes = digest.digest((value == null ? "" : value).getBytes(java.nio.charset.StandardCharsets.UTF_8));