
前言:
首先简述一下业务与问题
getUserTotalScoreList 接口是获取当前考试考生的总得分值列表信息(读数据)
getItemTotalScore 接口是自动计算当前考试考生的对应类型题目的总分值(写数据)
目前前端是需要同时刷新页面并同时调用以上两个接口 ,由于第一个接口是读数据,第二个接口是写数据,导致同步刷新获取的时候,数据不一致,getItemTotalScore 计算完成后进行返回考生 score 为18,之后从 getUserTotalScoreList 进行获取时还是老数据 score 为15 ,需要再次调用这个接口才能获取最新的 score 18
由上可知遇到了典型的数据不一致问题,前提是前端那边不进行串行调用接口的情况下
以下我是使用的 分布式锁 + Redis状态标记 进行处理的
这里是Redisson 分布式锁工具类 以及 定义了一个Redis 常量接口
java
/**
*
* 分布式锁工具类
*
*/
public class RedissonUtil {
// 计算得分方法正在进行
public static final String RUNNING = "0";
// 计算得分方法已经完成
public static final String FINISHED = "1";
/**
* 获取锁
*/
static public RLock getLock(String key, RedissonClient redissonClient) {
return redissonClient.getLock("exam:score:rwlock:" + key);
}
}
/**
*
* Redis 常量接口
*
*/
public interface RedisConstants {
/**
* 判断计算得分是否已经完成
*/
String SCORE_TASK_FINISHED_KEY = "score_task_finished:";
}
这里是 getItemTotalScore 方法**(写操作)**
首先是使用 Redisson 中的分布式锁方法进行加锁,若加锁失败,意味着当前 **(
**examId,uerId )
正在被其他线程处理或处于竞争状态,这时进行相关处理
若加锁成功,需基于 examId + uerId 为 key 来进行 redis 的状态标记处理(注意这里的 key 必须保证唯一,避免全局大锁),向其他人告知这个考生的成绩正在计算中**(Running)**
在计算考生成绩结束后,再修改当前 key 的状态为结束,告知其他人"我"已经完成这位考生的分数计算**(Finished)**
最后释放锁
java
@Resource
private RedissonClient redissonClient;
@Resource
private RedisTemplate<String, Object> redisTemplate;
public UserAnswerChooseAndEssayDTO getItemTotalScore(Integer examId, Integer userId) {
// 【解决前端同时刷新调用接口的并发问题】
// 1.获取 redisson 锁
RLock lock = RedissonUtil.getLock(examId + ":" + userId, redissonClient);
try {
// 1.1 尝试加锁,等待最多15秒,持有锁最多30秒
boolean isLocked = lock.tryLock(15, 30, TimeUnit.SECONDS);
if (!isLocked) {
String status = (String) redisTemplate.opsForValue().get(RedisConstants.SCORE_TASK_FINISHED_KEY + examId + ":" + userId);
if (RUNNING.equals(status)) {
throw new MyException(HttpStatus.SERVICE_UNAVAILABLE.value(), "成绩正在计算中,请稍后重试!");
} else {
throw new MyException(HttpStatus.REQUEST_TIMEOUT.value(), "成绩计算被阻塞,请重试!");
}
}
// 1.2 进行标记,表示写操作正在运行中
redisTemplate.opsForValue().set(RedisConstants.SCORE_TASK_FINISHED_KEY + examId + ":" + userId, RUNNING);
///////////// 这是计算过程代码.......
// 10.1 进行标记,表示写操作已经运行完成
redisTemplate.opsForValue().set(RedisConstants.SCORE_TASK_FINISHED_KEY + examId + ":" + userId, FINISHED);
return resultDTO;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new MyException(HttpStatus.INTERNAL_SERVER_ERROR.value(), "计算过程被中断!");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}}
这里是 getUserTotalScoreList 方法(读操作)
由于需要避免数据不同步问题,读操作肯定需要比写操作要**"慢"**,才能保证数据的实时/最终一致性
首先,我这里是设置最大循环重试三次(根据自己项目的响应情况来)
如果从 Redis 状态标记中获取当前 examId + userId key 状态为 Running的话,则表示还在计算中,这时需要进行等待(我这里是设置等待时间为 700 毫秒)
若在循环重试的过程中,发现其状态为 Finished了,则表示当前考试考生的得分计算完成了,跳出
接着,再次进行获取 Redis状态标记,查看是否还在计算中,进行相关处理
最后,将当前考试考生已经完成计算的旧数据状态给 delete 删除(注意,状态删除操作建议写在写操作的方法中,我这里是根据业务需求),接下来我是直接进行返回查询了,如果有其他的情况或更严谨的流程,可以在相应的模块进行加固代码逻辑
java
@Resource
private RedisTemplate<String, Object> redisTemplate;
// 重试的次数
public static final int MAX_RETRY_TIMES = 3;
// 每次等待的时间(毫秒)
public static final long WAIT_MILLIS = 700;
public List<UserExamCustom> getUserTotalScoreList(Integer examId, Integer userId) {
// 【解决前端同时刷新调用接口的并发问题】
// 1.首先进行循环检查,若得分计算已完成则直接返回
for (int i = 0; i < MAX_RETRY_TIMES; i++) {
String status = redisTemplate.opsForValue().get(RedisConstants.SCORE_TASK_FINISHED_KEY + examId + ":" + userId);
if (status == null || Objects.equals(status, RUNNING)) {
try {
Thread.sleep(WAIT_MILLIS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new MyException(HttpStatus.SERVICE_UNAVAILABLE.value(), "成绩查询被中断,请稍后重试!");
}
} else if (Objects.equals(status, FINISHED)) {
break;
}
}
// 2.循环完毕,若还未完成,则进行提示,其他情况直接返回
if (Objects.equals(redisTemplate.opsForValue().get(RedisConstants.SCORE_TASK_FINISHED_KEY + examId + ":" + userId), RUNNING)) {
throw new MyException(HttpStatus.BAD_REQUEST.value(), "成绩正在计算中,请稍后刷新页面!");
} else {
// 2.1 将当前考试考生已经完成计算的旧数据状态给删除
redisTemplate.delete(RedisConstants.SCORE_TASK_FINISHED_KEY + examId + ":" + userId);
return userExamMapper.selectByExamId(examId);
}}