Browse Source

统计考卷数据加锁

master
luogw 3 months ago
parent
commit
94dc8bcb0e
  1. 77
      src/main/java/com/project/statistics/domain/service/impl/SaveSaveErrorProneStatisticsDomainServiceImpl.java

77
src/main/java/com/project/statistics/domain/service/impl/SaveSaveErrorProneStatisticsDomainServiceImpl.java

@ -1,7 +1,6 @@
package com.project.statistics.domain.service.impl; package com.project.statistics.domain.service.impl;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.project.exam.domain.entity.ExamRecordEntity; import com.project.exam.domain.entity.ExamRecordEntity;
import com.project.question.domain.entity.QuestionEntity; 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 com.project.task.domain.enums.QuestionTypeEnum;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -22,6 +22,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Service @Service
@Slf4j @Slf4j
@ -36,6 +37,17 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP
@Autowired @Autowired
private TaskKnowledgePointBaseService taskKnowledgePointBaseService; 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 @Override
@Async @Async
public void collectErrorProneStatistics(Long taskId, Integer examMode, List<ExamRecordEntity.QuestionSnapshot> snapshots) { public void collectErrorProneStatistics(Long taskId, Integer examMode, List<ExamRecordEntity.QuestionSnapshot> snapshots) {
@ -78,6 +90,14 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP
continue; continue;
} }
String lockKey = LOCK_PREFIX + "gen:" + taskId + ":" + kp.getId();
boolean locked = tryLock(lockKey);
if (!locked) {
log.warn("[易错统计] 获取锁失败, lockKey={}, 题目ID={}", lockKey, questionId);
continue;
}
try {
ErrorProneStatisticsEntity entity = errorProneStatisticsBaseService.lambdaQuery() ErrorProneStatisticsEntity entity = errorProneStatisticsBaseService.lambdaQuery()
.eq(ErrorProneStatisticsEntity::getTaskId, taskId) .eq(ErrorProneStatisticsEntity::getTaskId, taskId)
.eq(ErrorProneStatisticsEntity::getKpId, kp.getId()) .eq(ErrorProneStatisticsEntity::getKpId, kp.getId())
@ -91,10 +111,16 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP
entity.setKpId(kp.getId()); entity.setKpId(kp.getId());
entity.setErrorCount(0); entity.setErrorCount(0);
entity.setAppearCount(0); entity.setAppearCount(0);
entity.setQuestionIdList(new ArrayList<>());
} }
List<Long> questionIdList = CollectionUtil.isEmpty(entity.getQuestionIdList()) ? new ArrayList<>() : entity.getQuestionIdList(); List<Long> questionIdList = entity.getQuestionIdList();
if (questionIdList == null) {
questionIdList = new ArrayList<>();
}
if (!questionIdList.contains(questionId)) {
questionIdList.add(questionId); questionIdList.add(questionId);
}
entity.setQuestionIdList(questionIdList); entity.setQuestionIdList(questionIdList);
entity.setAppearCount(entity.getAppearCount() + 1); entity.setAppearCount(entity.getAppearCount() + 1);
if (Boolean.FALSE.equals(snapshot.getIsRight())) { if (Boolean.FALSE.equals(snapshot.getIsRight())) {
@ -102,6 +128,9 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP
} }
errorProneStatisticsBaseService.saveOrUpdate(entity); errorProneStatisticsBaseService.saveOrUpdate(entity);
} finally {
unlock(lockKey);
}
} }
} }
} }
@ -121,15 +150,24 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP
continue; continue;
} }
String lockKey = LOCK_PREFIX + "classic:" + taskId + ":" + snapshot.getQuestionId();
boolean locked = tryLock(lockKey);
if (!locked) {
log.warn("[易错统计] 获取锁失败, lockKey={}, 题目ID={}", lockKey, snapshot.getQuestionId());
continue;
}
try {
ErrorProneStatisticsEntity entity = errorProneStatisticsBaseService.lambdaQuery() ErrorProneStatisticsEntity entity = errorProneStatisticsBaseService.lambdaQuery()
.eq(ErrorProneStatisticsEntity::getTaskId, taskId) .eq(ErrorProneStatisticsEntity::getTaskId, taskId)
.eq(ErrorProneStatisticsEntity::getExamMode, snapshot.getQuestionId()) .eq(ErrorProneStatisticsEntity::getExamMode, ExamModeEnum.CLASSIC.getValue())
.apply("JSON_CONTAINS(question_id_list, CAST({0} AS JSON))", snapshot.getQuestionId())
.one(); .one();
if (entity == null) { if (entity == null) {
entity = new ErrorProneStatisticsEntity(); entity = new ErrorProneStatisticsEntity();
entity.setTaskId(taskId); entity.setTaskId(taskId);
entity.setQuestionIdList(List.of(snapshot.getQuestionId())); entity.setQuestionIdList(new ArrayList<>(List.of(snapshot.getQuestionId())));
entity.setExamMode(ExamModeEnum.CLASSIC.getValue()); entity.setExamMode(ExamModeEnum.CLASSIC.getValue());
entity.setContent(content); entity.setContent(content);
entity.setQuestionType(snapshot.getType()); entity.setQuestionType(snapshot.getType());
@ -146,8 +184,39 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP
updateOptionDetail(entity, snapshot.getUserAnswer()); updateOptionDetail(entity, snapshot.getUserAnswer());
errorProneStatisticsBaseService.saveOrUpdate(entity); 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);
}
/** /**
* 构建选项详情 * 构建选项详情

Loading…
Cancel
Save