后端两个接口需分开写,前端需不串行并同时刷新调用但数据不同步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);
}}
相关推荐
音符犹如代码6 小时前
Java并发List实战:CopyOnWriteArrayList原理与ArrayList常见面试题
java·开发语言·面试·list
代码or搬砖6 小时前
Docker 部署 Java 项目实践
java·docker·容器
又是忙碌的一天6 小时前
抽象类和接口
java·开发语言
August_._6 小时前
【MySQL】SQL语法详细总结
java·数据库·后端·sql·mysql·oracle
Dxxyyyy7 小时前
零基础学JAVA--Day26(枚举类)
java·开发语言
黑屋里的马7 小时前
java的设计模式之桥接模式(Bridge)
java·算法·桥接模式
升鲜宝供应链及收银系统源代码服务7 小时前
升鲜宝生鲜配送供应链管理系统---PMS--商品品牌多语言存储与 Redis 缓存同步实现
java·开发语言·数据库·redis·缓存·开源·供应链系统
练习时长一年7 小时前
Spring AoP的切点匹配
java·开发语言
27669582927 小时前
朴朴超市小程序分析
java·python·小程序·node·sign·朴朴超市·sign-v2
源码宝7 小时前
企业项目级医院随访系统源码,患者随访管理系统,技术框架:Java+Spring boot,Vue,Ant-Design+MySQL5
java·vue.js·spring·程序·医院管理系统·随访·随访系统源码