后端两个接口需分开写,前端需不串行并同时刷新调用但数据不同步NOTE

前言:

首先简述一下业务与问题

getUserTotalScoreList 接口是获取当前考试考生的总得分值列表信息(读数据)

getItemTotalScore 接口是自动计算当前考试考生的对应类型题目的总分值(写数据)

目前前端是需要同时刷新页面并同时调用以上两个接口 ,由于第一个接口是读数据,第二个接口是写数据,导致同步刷新获取的时候,数据不一致,getItemTotalScore 计算完成后进行返回考生 score18,之后从 getUserTotalScoreList 进行获取时还是老数据 score15 ,需要再次调用这个接口才能获取最新的 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 + uerIdkey 来进行 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);
}}
相关推荐
一叶飘零_sweeeet3 小时前
极简 Go 语言教程:从 Java 开发者视角 3 小时入门实战
java·开发语言·golang
失散133 小时前
分布式专题——21 Kafka客户端消息流转流程
java·分布式·云原生·架构·kafka
xiaoye37083 小时前
Spring Boot 详细介绍
java·spring boot·后端
我不是混子3 小时前
如何实现数据脱敏?
java·后端
野犬寒鸦4 小时前
今日面试之项目拷打:锁与事务的深度解析
java·服务器·数据库·后端
ajassi20004 小时前
开源 java android app 开发(十五)自定义绘图控件--仪表盘
android·java·开源
FrankYoou4 小时前
Spring Boot 自动配置之 TaskExecutor
java·spring boot
爱读源码的大都督4 小时前
Spring AI Alibaba JManus底层实现剖析
java·人工智能·后端
间彧4 小时前
ReentrantLock与ReadWriteLock在性能和使用场景上有什么区别?
java