task-54: finalRowCount 改为任务内增量计数(upsert newlyInserted 累计),移除每 chunk 全表 COUNT(*)

This commit is contained in:
2026-08-30 16:41:02 +08:00
parent e8cedbcb50
commit 91f760f789
3 changed files with 282 additions and 18 deletions
@@ -666,8 +666,12 @@ public class CollectDataService {
requireRustfsPayload(storedDetail, "采集结果明细必须写入 RustFS");
// 批量 upsert:先一次批量查现有行(payload_hash 相等即跳过,幂等),
// 再按唯一键 uk_task_scope_item 分批发 INSERT ... ON DUPLICATE KEY UPDATE。
resultItemBatchWriter.upsertAccepted(task.getId(), result.getId(), scopeKey,
chunkIndex, accepted, storedDetail);
// newlyInserted 是本 chunk 真实新增的行数,任务内增量累计得到 finalRowCount
// 替代每个 chunk 一次全表 COUNT(*),且与 chunk 乱序/重提无关。
CollectDataResultItemBatchWriter.UpsertCounts upsertCounts =
resultItemBatchWriter.upsertAccepted(task.getId(), result.getId(), scopeKey,
chunkIndex, accepted, storedDetail);
stats.finalRowCount += upsertCounts.newlyInserted();
}
String payloadJson = writeJson(rows, "采集结果序列化失败");
@@ -677,7 +681,6 @@ public class CollectDataService {
persistChunk(taskId, scopeKey, scopeHash, chunkIndex, chunkTotal, storedPayload, payloadJson);
persistScope(taskId, scopeKey, scopeHash, chunkTotal, request);
stats.finalRowCount = countFinalRows(taskId);
// Python 在 done=true 那次回传携带关键词级聚合统计;非空时按"最后一次为准"覆盖。
List<CollectDataSummaryRowDto> incomingSummaries = request.getSummaries();
if (incomingSummaries != null && !incomingSummaries.isEmpty()) {
@@ -946,16 +949,6 @@ public class CollectDataService {
return out;
}
private int countFinalRows(Long taskId) {
if (taskId == null || taskId <= 0) {
return 0;
}
Long count = taskResultItemMapper.selectCount(new LambdaQueryWrapper<TaskResultItemEntity>()
.eq(TaskResultItemEntity::getTaskId, taskId)
.eq(TaskResultItemEntity::getModuleType, MODULE_TYPE));
return count == null ? 0 : count.intValue();
}
private void ensureRustfsPayloadStorageEnabled() {
if (!transientPayloadStorageService.isSharedWriteEnabled()) {
throw new BusinessException("RustFS 未配置,采集结果回传暂不可接收");
@@ -1011,7 +1004,6 @@ public class CollectDataService {
CollectDataSubmitResultRequest request,
int currentChunkRows) {
CollectDataStats stats = loadStats(task);
stats.finalRowCount = countFinalRows(task == null ? null : task.getId());
CollectDataSubmitResultVo vo = new CollectDataSubmitResultVo();
vo.setTaskId(task == null ? null : task.getId());
vo.setResultId(result == null ? null : result.getId());
@@ -45,15 +45,21 @@ public class CollectDataResultItemBatchWriter {
this.batchSize = batchSize <= 0 ? DEFAULT_BATCH_SIZE : batchSize;
}
/** 批量写入计数:insertedOrUpdated 实际写入行数,skipped hash 相等跳过行数。 */
public record UpsertCounts(int insertedOrUpdated, int skipped) {
/**
* 批量写入计数:
* insertedOrUpdated 由 mapper 返回的 affected 行数累加(INSERT=1、存量更新可能为 2,
* 仅用于诊断日志);
* newlyInserted 是本次调用真实新增的行数(仅原本不存在的行,hash 相等跳过与存量
* 更新均不计入,批量失败扣除未写入的新行),供调用方做任务内 finalRowCount 增量累计。
*/
public record UpsertCounts(int insertedOrUpdated, int skipped, int newlyInserted) {
}
/** 把整 chunk 的 accepted 行批量 upsertscopeKey 与 chunk 内 offset 已知,仅计算 refJson 与 hash。 */
public UpsertCounts upsertAccepted(Long taskId, Long resultId, String scopeKey, int chunkIndex,
List<CollectDataResultRowVo> rows, String storedDetail) {
if (rows == null || rows.isEmpty()) {
return new UpsertCounts(0, 0);
return new UpsertCounts(0, 0, 0);
}
String scopeHash = sha256(scopeKey);
// 一次性取回本 scope 现有行,构建 item_key → 现有行 映射(hash 相等即跳过)。
@@ -71,6 +77,9 @@ public class CollectDataResultItemBatchWriter {
List<TaskResultItemEntity> toUpsert = new ArrayList<>();
int skipped = 0;
// 仅原本不存在的行(真 INSERT)计入 newlyInserted;存量行 hash 不同触发 UPDATE
// 时表内行数不变,不计入,避免 finalRowCount 增量虚高。
int newlyInserted = 0;
LocalDateTime now = LocalDateTime.now();
// 批量生成整 chunk 的引用 JSON + hash(一次迭代),替代逐行 encodeRef + hash。
List<CollectDataResultDetailCodec.RefWithHash> refsWithHash =
@@ -111,6 +120,9 @@ public class CollectDataResultItemBatchWriter {
entity.setCreatedAt(existing == null ? now : existing.getCreatedAt());
entity.setUpdatedAt(now);
toUpsert.add(entity);
if (existing == null) {
newlyInserted++;
}
}
int written = 0;
@@ -122,9 +134,16 @@ public class CollectDataResultItemBatchWriter {
} catch (RuntimeException ex) {
log.warn("[collect-data] upsert result item batch failed, skip batch {}..{} taskId={}",
from, to, taskId, ex);
// 失败批的新行未落库,从增量计数中扣除,避免任务内累计虚高;
// 重提该 chunk 时按存量 hash 跳过已落库行、补插未落库行,累计收敛到真实行数。
for (TaskResultItemEntity entity : batch) {
if (entity.getId() == null) {
newlyInserted--;
}
}
}
}
return new UpsertCounts(written, skipped);
return new UpsertCounts(written, skipped, newlyInserted);
}
private static String sha256(String value) {