From 94dc8bcb0ed2343d134eefc5a9535758894fe7f3 Mon Sep 17 00:00:00 2001 From: luogw <3132758203@qq.com> Date: Wed, 17 Jun 2026 10:47:55 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E8=80=83=E5=8D=B7=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=8A=A0=E9=94=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ErrorProneStatisticsDomainServiceImpl.java | 155 +++++++++++++----- 1 file changed, 112 insertions(+), 43 deletions(-) diff --git a/src/main/java/com/project/statistics/domain/service/impl/SaveSaveErrorProneStatisticsDomainServiceImpl.java b/src/main/java/com/project/statistics/domain/service/impl/SaveSaveErrorProneStatisticsDomainServiceImpl.java index fc2d611..53afc2e 100644 --- a/src/main/java/com/project/statistics/domain/service/impl/SaveSaveErrorProneStatisticsDomainServiceImpl.java +++ b/src/main/java/com/project/statistics/domain/service/impl/SaveSaveErrorProneStatisticsDomainServiceImpl.java @@ -1,7 +1,6 @@ package com.project.statistics.domain.service.impl; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; import com.project.exam.domain.entity.ExamRecordEntity; import com.project.question.domain.entity.QuestionEntity; @@ -15,6 +14,7 @@ import com.project.task.domain.enums.ExamModeEnum; import com.project.task.domain.enums.QuestionTypeEnum; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.TimeUnit; @Service @Slf4j @@ -36,6 +37,17 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP @Autowired private TaskKnowledgePointBaseService taskKnowledgePointBaseService; + @Autowired + private StringRedisTemplate redisTemplate; + + private static final String LOCK_PREFIX = "lock:error_prone:"; + // 锁自动过期时间5秒,防止死锁(拿到锁后程序崩溃没释放,5s自动失效) + private static final int LOCK_EXPIRE_SECONDS = 5; + // 自旋最大重试次数:最多抢锁10次 + private static final int LOCK_RETRY_TIMES = 10; + // 每次抢锁失败后,休眠50ms再重试 + private static final long LOCK_RETRY_INTERVAL_MS = 50; + @Override @Async public void collectErrorProneStatistics(Long taskId, Integer examMode, List snapshots) { @@ -78,30 +90,47 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP continue; } - ErrorProneStatisticsEntity entity = errorProneStatisticsBaseService.lambdaQuery() - .eq(ErrorProneStatisticsEntity::getTaskId, taskId) - .eq(ErrorProneStatisticsEntity::getKpId, kp.getId()) - .one(); - - if (entity == null) { - entity = new ErrorProneStatisticsEntity(); - entity.setTaskId(taskId); - entity.setExamMode(ExamModeEnum.GENERATIVE.getValue()); - entity.setContent(kp.getContent()); - entity.setKpId(kp.getId()); - entity.setErrorCount(0); - entity.setAppearCount(0); + String lockKey = LOCK_PREFIX + "gen:" + taskId + ":" + kp.getId(); + boolean locked = tryLock(lockKey); + if (!locked) { + log.warn("[易错统计] 获取锁失败, lockKey={}, 题目ID={}", lockKey, questionId); + continue; } - List questionIdList = CollectionUtil.isEmpty(entity.getQuestionIdList()) ? new ArrayList<>() : entity.getQuestionIdList(); - questionIdList.add(questionId); - entity.setQuestionIdList(questionIdList); - entity.setAppearCount(entity.getAppearCount() + 1); - if (Boolean.FALSE.equals(snapshot.getIsRight())) { - entity.setErrorCount(entity.getErrorCount() + 1); - } + try { + ErrorProneStatisticsEntity entity = errorProneStatisticsBaseService.lambdaQuery() + .eq(ErrorProneStatisticsEntity::getTaskId, taskId) + .eq(ErrorProneStatisticsEntity::getKpId, kp.getId()) + .one(); - errorProneStatisticsBaseService.saveOrUpdate(entity); + if (entity == null) { + entity = new ErrorProneStatisticsEntity(); + entity.setTaskId(taskId); + entity.setExamMode(ExamModeEnum.GENERATIVE.getValue()); + entity.setContent(kp.getContent()); + entity.setKpId(kp.getId()); + entity.setErrorCount(0); + entity.setAppearCount(0); + entity.setQuestionIdList(new ArrayList<>()); + } + + List questionIdList = entity.getQuestionIdList(); + if (questionIdList == null) { + questionIdList = new ArrayList<>(); + } + if (!questionIdList.contains(questionId)) { + questionIdList.add(questionId); + } + entity.setQuestionIdList(questionIdList); + entity.setAppearCount(entity.getAppearCount() + 1); + if (Boolean.FALSE.equals(snapshot.getIsRight())) { + entity.setErrorCount(entity.getErrorCount() + 1); + } + + errorProneStatisticsBaseService.saveOrUpdate(entity); + } finally { + unlock(lockKey); + } } } } @@ -121,32 +150,72 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP continue; } - ErrorProneStatisticsEntity entity = errorProneStatisticsBaseService.lambdaQuery() - .eq(ErrorProneStatisticsEntity::getTaskId, taskId) - .eq(ErrorProneStatisticsEntity::getExamMode, snapshot.getQuestionId()) - .one(); - - if (entity == null) { - entity = new ErrorProneStatisticsEntity(); - entity.setTaskId(taskId); - entity.setQuestionIdList(List.of(snapshot.getQuestionId())); - entity.setExamMode(ExamModeEnum.CLASSIC.getValue()); - entity.setContent(content); - entity.setQuestionType(snapshot.getType()); - entity.setErrorCount(0); - entity.setAppearCount(0); - entity.setOptionDetails(buildOptionDetail(snapshot)); + String lockKey = LOCK_PREFIX + "classic:" + taskId + ":" + snapshot.getQuestionId(); + boolean locked = tryLock(lockKey); + if (!locked) { + log.warn("[易错统计] 获取锁失败, lockKey={}, 题目ID={}", lockKey, snapshot.getQuestionId()); + continue; } - entity.setAppearCount(entity.getAppearCount() + 1); - if (Boolean.FALSE.equals(snapshot.getIsRight())) { - entity.setErrorCount(entity.getErrorCount() + 1); - } + try { + ErrorProneStatisticsEntity entity = errorProneStatisticsBaseService.lambdaQuery() + .eq(ErrorProneStatisticsEntity::getTaskId, taskId) + .eq(ErrorProneStatisticsEntity::getExamMode, ExamModeEnum.CLASSIC.getValue()) + .apply("JSON_CONTAINS(question_id_list, CAST({0} AS JSON))", snapshot.getQuestionId()) + .one(); - updateOptionDetail(entity, snapshot.getUserAnswer()); + if (entity == null) { + entity = new ErrorProneStatisticsEntity(); + entity.setTaskId(taskId); + entity.setQuestionIdList(new ArrayList<>(List.of(snapshot.getQuestionId()))); + entity.setExamMode(ExamModeEnum.CLASSIC.getValue()); + entity.setContent(content); + entity.setQuestionType(snapshot.getType()); + entity.setErrorCount(0); + entity.setAppearCount(0); + entity.setOptionDetails(buildOptionDetail(snapshot)); + } - errorProneStatisticsBaseService.saveOrUpdate(entity); + entity.setAppearCount(entity.getAppearCount() + 1); + if (Boolean.FALSE.equals(snapshot.getIsRight())) { + entity.setErrorCount(entity.getErrorCount() + 1); + } + + updateOptionDetail(entity, snapshot.getUserAnswer()); + + errorProneStatisticsBaseService.saveOrUpdate(entity); + } finally { + unlock(lockKey); + } + } + } + + /** + * 尝试获取 Redis 分布式锁(带自旋重试) + */ + private boolean tryLock(String lockKey) { + int retry = 0; + while (retry < LOCK_RETRY_TIMES) { + Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_EXPIRE_SECONDS, TimeUnit.SECONDS); + if (Boolean.TRUE.equals(locked)) { + return true; + } + retry++; + try { + Thread.sleep(LOCK_RETRY_INTERVAL_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } } + return false; + } + + /** + * 释放 Redis 分布式锁 + */ + private void unlock(String lockKey) { + redisTemplate.delete(lockKey); } /**