task-28: 以 scope 计数器替代每个 chunk 的 COUNT(*) 完整统计
分片接收不再对 chunk 表执行 selectCount 全量统计:新插入分片按 scope 计数器 +1 并钳制到 chunk_total,重复提交(唯一键冲突)保持不变;分片行与 scope 计数器 作为一致性单元,状态写入失败时回滚本次插入的分片行与 payload,重试走全新插入, 避免计数器重复计数或永久欠计。任务级分布式锁串行化同一任务的接收,读改写在锁内完成。
This commit is contained in:
+37
-8
@@ -1280,8 +1280,12 @@ public class ShopDataCrawlTaskService {
|
||||
chunk.setPayloadHash(payloadHash);
|
||||
chunk.setCreatedAt(now);
|
||||
chunk.setUpdatedAt(now);
|
||||
int receivedChunkCount;
|
||||
boolean insertedThisCall = false;
|
||||
try {
|
||||
taskChunkMapper.insert(chunk);
|
||||
insertedThisCall = true;
|
||||
receivedChunkCount = nextScopeChunkCount(scope, chunkTotal);
|
||||
} catch (DuplicateKeyException ex) {
|
||||
transientPayloadStorageService.deletePayloadIfPresent(storedPayload);
|
||||
TaskChunkEntity winner = findResultChunk(taskId, scopeHash, chunkIndex);
|
||||
@@ -1289,13 +1293,27 @@ public class ShopDataCrawlTaskService {
|
||||
throw new BusinessException("店铺数据抓取结果分片并发写入失败,请重试");
|
||||
}
|
||||
validateExistingChunk(winner, chunkTotal, payloadHash);
|
||||
receivedChunkCount = scopeChunkCount(scope);
|
||||
} catch (RuntimeException ex) {
|
||||
transientPayloadStorageService.deletePayloadIfPresent(storedPayload);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
int receivedChunkCount = countResultChunks(taskId, scopeHash);
|
||||
persistResultScope(taskId, scopeKey, scopeHash, chunkTotal, receivedChunkCount);
|
||||
try {
|
||||
persistResultScope(taskId, scopeKey, scopeHash, chunkTotal, receivedChunkCount);
|
||||
} catch (RuntimeException ex) {
|
||||
// 分片行与 scope 计数器是一致性单元:状态写入失败则回滚本次插入的分片行与 payload,
|
||||
// 客户端重试会走全新插入路径,计数器不会因重试重复计数或永久欠计。
|
||||
if (insertedThisCall) {
|
||||
taskChunkMapper.delete(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||
.eq(TaskChunkEntity::getTaskId, taskId)
|
||||
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(TaskChunkEntity::getScopeHash, scopeHash)
|
||||
.eq(TaskChunkEntity::getChunkIndex, chunkIndex));
|
||||
transientPayloadStorageService.deletePayloadIfPresent(storedPayload);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
log.info("[shop-data-crawl] result chunk received taskId={} shop={} chunk={}/{} received={}",
|
||||
taskId, shopKey, chunkIndex, chunkTotal, receivedChunkCount);
|
||||
return new ResultChunkReceipt(scopeHash, chunkTotal, receivedChunkCount >= chunkTotal);
|
||||
@@ -1343,12 +1361,23 @@ public class ShopDataCrawlTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
private int countResultChunks(Long taskId, String scopeHash) {
|
||||
Long count = taskChunkMapper.selectCount(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||
.eq(TaskChunkEntity::getTaskId, taskId)
|
||||
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(TaskChunkEntity::getScopeHash, scopeHash));
|
||||
return count == null ? 0 : count.intValue();
|
||||
/**
|
||||
* 以 scope 计数器替代 chunk 表全量 COUNT(*):新分片插入成功后 +1 并钳制到 chunk_total。
|
||||
* 任务级分布式锁串行化同一任务的接收,读改写在单锁内完成,无需再统计 chunk 行数。
|
||||
*/
|
||||
private int nextScopeChunkCount(TaskScopeStateEntity scope, int chunkTotal) {
|
||||
int previous = scope == null ? 0 : Math.max(0, scope.getReceivedChunkCount() == null ? 0 : scope.getReceivedChunkCount());
|
||||
return Math.min(chunkTotal, previous + 1);
|
||||
}
|
||||
|
||||
/** 重复提交(唯一键冲突,分片已计过数)时计数器保持不变,钳制到 chunk_total。 */
|
||||
private int scopeChunkCount(TaskScopeStateEntity scope) {
|
||||
if (scope == null || scope.getReceivedChunkCount() == null) {
|
||||
return 0;
|
||||
}
|
||||
int count = Math.max(0, scope.getReceivedChunkCount());
|
||||
Integer chunkTotal = scope.getChunkTotal();
|
||||
return chunkTotal != null && chunkTotal > 0 ? Math.min(chunkTotal, count) : count;
|
||||
}
|
||||
|
||||
private void persistResultScope(Long taskId,
|
||||
|
||||
Reference in New Issue
Block a user