🤵♂️ 个人主页:@rain雨雨编程
😄微信公众号:rain雨雨编程
✍🏻作者简介:持续分享机器学习,爬虫,数据分析
🐋 希望大家多多支持,我们一起进步!
如果文章对你有帮助的话,
欢迎评论 💬点赞👍🏻 收藏 📂加关注+
高性能题库管理系统的开发实践
在教育领域,题库管理系统是教学和考试中不可或缺的工具。它不仅需要高效地管理大量题目,还要支持灵活的查询、编辑和批量操作。本文将通过一个实际的题库管理系统项目,分享其开发过程、技术栈的选型与应用,以及核心功能的实现思路。
目录
一、项目介绍
项目背景
随着在线教育和远程考试的普及,题库管理系统的需求日益增长。传统的题库管理系统往往功能单一,且在高并发场景下表现不佳。因此,我们开发了一个高性能、高并发的题库管理系统,旨在满足教育机构、在线学习平台以及个人用户的需求。
功能概述
本题库管理系统具备以下核心功能:
-
题目查询:支持根据题库、关键词、题型、难度等条件查询题目列表。
-
题库详情:查看题库的基本信息和题目列表。
-
编辑题目:修改题目所属题库。
-
批量操作:支持批量添加、移除和删除题目。
技术目标
-
高并发支持:系统能够处理高并发请求,保证响应速度。
-
数据一致性:通过事务管理确保数据操作的正确性。
-
异步处理:优化系统性能,减少用户等待时间。
-
可扩展性:系统设计支持未来功能扩展。
二、技术栈指点总结
在开发高并发题库管理系统时,我们重点关注了线程池、异步处理、并发控制和事务管理等关键技术点。以下是技术栈的详细总结:
1. 线程池
线程池是提高系统性能和资源利用率的关键技术。我们使用了 Java 的 ThreadPoolExecutor
来管理线程池,通过合理配置核心线程数、最大线程数和队列容量,确保系统在高并发场景下能够高效处理请求。
示例代码:
ExecutorService threadPool = new ThreadPoolExecutor(
10, // 核心线程数
50, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100) // 任务队列容量
);
2. 异步处理
为了提升用户体验,我们采用了异步处理机制。通过 Java 的 CompletableFuture
和 Spring 的 @Async
注解,将一些耗时操作(如批量操作)异步执行,避免阻塞主线程。
示例代码:
@Async
public CompletableFuture<Void> batchProcessQuestions(List<Question> questions) {
questions.forEach(question -> {
// 异步执行题目处理逻辑
processQuestion(question);
});
return CompletableFuture.completedFuture(null);
}
3. 并发控制
在高并发场景下,数据一致性至关重要。我们使用了乐观锁和悲观锁机制来控制并发操作。对于一些高频更新的操作(如修改题目所属题库),我们采用了乐观锁,通过版本号(version
)来避免并发冲突。
示例代码:
int updateRows = questionMapper.updateQuestion(question, version);
if (updateRows == 0) {
throw new OptimisticLockException("并发冲突,操作失败");
}
4. 事务管理
为了保证数据操作的正确性,我们使用了 Spring 的事务管理机制。通过 @Transactional
注解,确保关键操作(如批量删除题目)在事务中执行,避免数据不一致问题。
示例代码:
@Transactional(rollbackFor = Exception.class)
public void batchDeleteQuestions(List<Long> questionIds) {
questionIds.forEach(questionMapper::deleteQuestionById);
}
三、核心代码
以下是题库管理系统的核心功能介绍,涵盖题目查询、题库详情获取、题目编辑以及批量操作等功能。
3.1 根据题库id查询题目列表
功能介绍
该功能允许用户根据题库 ID 查询题目列表,并支持通过关键词、题型等条件进行过滤。通过合理的索引设计和查询优化,系统能够快速返回查询结果,即使在数据量较大的情况下也能保持高效。
public Page<Question> listQuestionByPage(QuestionQueryRequest questionQueryRequest) {
long current = questionQueryRequest.getCurrent();
long size = questionQueryRequest.getPageSize();
// 题目表的查询条件
QueryWrapper<Question> queryWrapper = this.getQueryWrapper(questionQueryRequest);
// 根据题库查询题目列表接口
Long questionBankId = questionQueryRequest.getQuestionBankId();
if (questionBankId != null) {
// 查询题库内的题目 id
LambdaQueryWrapper<QuestionBankQuestion> lambdaQueryWrapper = Wrappers.lambdaQuery(QuestionBankQuestion.class)
.select(QuestionBankQuestion::getQuestionId)
.eq(QuestionBankQuestion::getQuestionBankId, questionBankId);
List<QuestionBankQuestion> questionList = questionBankQuestionService.list(lambdaQueryWrapper);
if (CollUtil.isNotEmpty(questionList)) {
// 取出题目 id 集合
Set<Long> questionIdSet = questionList.stream()
.map(QuestionBankQuestion::getQuestionId)
.collect(Collectors.toSet());
// 复用原有题目表的查询条件
queryWrapper.in("id", questionIdSet);
}
}
// 查询数据库
Page<Question> questionPage = this.page(new Page<>(current, size), queryWrapper);
return questionPage;
}
3.2 获取题库详情接口
功能介绍
该接口用于获取题库的详细信息,包括题库的基本信息(如名称、描述、创建时间等)以及该题库下的所有题目列表。通过分页机制,用户可以方便地浏览大量题目,而不会对性能造成过大压力。
@GetMapping("/get/vo")
public BaseResponse<QuestionBankVO> getQuestionBankVOById(QuestionBankQueryRequest questionBankQueryRequest, HttpServletRequest request) {
ThrowUtils.throwIf(questionBankQueryRequest == null, ErrorCode.PARAMS_ERROR);
Long id = questionBankQueryRequest.getId();
ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);
// 查询数据库
QuestionBank questionBank = questionBankService.getById(id);
ThrowUtils.throwIf(questionBank == null, ErrorCode.NOT_FOUND_ERROR);
// 查询题库封装类
QuestionBankVO questionBankVO = questionBankService.getQuestionBankVO(questionBank, request);
// 是否要关联查询题库下的题目列表
boolean needQueryQuestionList = questionBankQueryRequest.isNeedQueryQuestionList();
if (needQueryQuestionList) {
QuestionQueryRequest questionQueryRequest = new QuestionQueryRequest();
questionQueryRequest.setQuestionBankId(id);
Page<Question> questionPage = questionService.listQuestionByPage(questionQueryRequest);
questionBankVO.setQuestionPage(questionPage);
}
// 获取封装类
return ResultUtils.success(questionBankVO);
}
3.3 修改题目所属题库接口
功能介绍
该功能允许用户将题目从一个题库移动到另一个题库。通过乐观锁机制,我们确保在高并发场景下不会出现数据冲突。此外,系统会自动检查题目是否存在以及目标题库是否有效,从而保证操作的正确性。
@Override
public void validQuestionBankQuestion(QuestionBankQuestion questionBankQuestion, boolean add) {
ThrowUtils.throwIf(questionBankQuestion == null, ErrorCode.PARAMS_ERROR);
// 题目和题库必须存在
Long questionId = questionBankQuestion.getQuestionId();
if (questionId != null) {
Question question = questionService.getById(questionId);
ThrowUtils.throwIf(question == null, ErrorCode.NOT_FOUND_ERROR, "题目不存在");
}
Long questionBankId = questionBankQuestion.getQuestionBankId();
if (questionBankId != null) {
QuestionBank questionBank = questionBankService.getById(questionBankId);
ThrowUtils.throwIf(questionBank == null, ErrorCode.NOT_FOUND_ERROR, "题库不存在");
}
}
@PostMapping("/remove")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> removeQuestionBankQuestion(
@RequestBody QuestionBankQuestionRemoveRequest questionBankQuestionRemoveRequest
) {
// 参数校验
ThrowUtils.throwIf(questionBankQuestionRemoveRequest == null, ErrorCode.PARAMS_ERROR);
Long questionBankId = questionBankQuestionRemoveRequest.getQuestionBankId();
Long questionId = questionBankQuestionRemoveRequest.getQuestionId();
ThrowUtils.throwIf(questionBankId == null || questionId == null, ErrorCode.PARAMS_ERROR);
// 构造查询
LambdaQueryWrapper<QuestionBankQuestion> lambdaQueryWrapper = Wrappers.lambdaQuery(QuestionBankQuestion.class)
.eq(QuestionBankQuestion::getQuestionId, questionId)
.eq(QuestionBankQuestion::getQuestionBankId, questionBankId);
boolean result = questionBankQuestionService.remove(lambdaQueryWrapper);
return ResultUtils.success(result);
}
3.4 批量向题库中添加题目
功能介绍
该功能允许用户一次性将多个题目添加到指定题库中。通过异步处理机制,系统可以在后台批量处理这些题目,而不会阻塞用户界面。同时,我们使用事务管理确保所有操作要么全部成功,要么全部失败,从而保证数据一致性。
/**
* 批量添加题目到题库
* @param questionIdList
* @param questionBankId
// * @param loginUser
*/
@Override
public void bathcAddQuestionsToBank(List<Long> questionIdList, Long questionBankId, User loginUser) {
// 参数校验
ThrowUtils.throwIf(CollUtil.isEmpty(questionIdList), ErrorCode.PARAMS_ERROR,"题目列表为空");
ThrowUtils.throwIf(questionBankId == null || questionBankId<=0, ErrorCode.PARAMS_ERROR,"题库id非法");
ThrowUtils.throwIf(loginUser == null, ErrorCode.NOT_LOGIN_ERROR,"用户未登录");
// 题目id是否存在
// List<Question> questionList = questionService.listByIds(questionIdList);
//改进1:不用查所有的题目,只需要查合法的题目id是否存在即可
LambdaQueryWrapper<Question> questionLambdaQueryWrapper = Wrappers.lambdaQuery(Question.class)
.select(Question::getId)
.in(Question::getId, questionIdList);
// List<Question> questionList = questionService.list(questionLambdaQueryWrapper);
// List<Long> validQuestionIdList = questionList.stream()
// .map(Question::getId).collect(Collectors.toList());
//改进2:对以上代码进行改进,因为返回值只有id一列,可以直接转为Long列表,不让框架封装结果为Question对象,减少内存消耗
List<Long> validQuestionIdList = questionService.listObjs(questionLambdaQueryWrapper, obj -> (Long) obj);
ThrowUtils.throwIf(CollUtil.isEmpty(validQuestionIdList), ErrorCode.PARAMS_ERROR,"题目id非法");
// 检查哪些题目id已经存在题库中
LambdaQueryWrapper<QuestionBankQuestion> lambdaQueryWrapper = Wrappers.lambdaQuery(QuestionBankQuestion.class)
.eq(QuestionBankQuestion::getQuestionBankId, questionBankId)
.in(QuestionBankQuestion::getQuestionId, validQuestionIdList);
List<QuestionBankQuestion> existsQuestionBankQuestions = this.list(lambdaQueryWrapper);
Set<Long> existsQuestionBankQuestionIdList = existsQuestionBankQuestions.stream()
.map(QuestionBankQuestion::getQuestionId)
.collect(Collectors.toSet());
// 已经存在题库的题目,不必重复添加
validQuestionIdList = validQuestionIdList.stream()
.filter(questionId -> {
return !existsQuestionBankQuestionIdList.contains(questionId);
}).collect(Collectors.toList());
// 题库id是否存在
QuestionBank questionBank = questionBankService.getById(questionBankId);
ThrowUtils.throwIf(questionBank == null, ErrorCode.PARAMS_ERROR,"题库id非法");
//自定义线程池,避免在高并发情况下,大量创建线程导致系统崩溃
ThreadPoolExecutor customExecutor = new ThreadPoolExecutor(
15,
40,
60L,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(10000),
new ThreadPoolExecutor.CallerRunsPolicy()
);
//保存所有线程任务到future列表中,便于后续统一处理结果
ArrayList<CompletableFuture<Void>> futures = new ArrayList<>();
//分批次处理,避免长事务,假设每次处理1000条数据
int batchSize = 1000;
int size = validQuestionIdList.size();
for(int i=0;i<size;i+=batchSize){
List<Long> subList = validQuestionIdList.subList(i, Math.min(i + batchSize, size));
List<QuestionBankQuestion> questionBankQuestions = subList.stream()
.map(questionId -> {
QuestionBankQuestion questionBankQuestion = new QuestionBankQuestion();
questionBankQuestion.setQuestionId(questionId);
questionBankQuestion.setQuestionBankId(questionBankId);
return questionBankQuestion;
}).collect(Collectors.toList());
QuestionBankQuestionService questionBankQuestionService = (QuestionBankQuestionServiceImpl) AopContext.currentProxy();
//异步处理每批数据,将任务添加到异步列表中
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
questionBankQuestionService.bathcAddQuestionsToBankInner(questionBankQuestions);
}, customExecutor);
futures.add(future);
}
//等待所有任务完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
//关闭线程池
customExecutor.shutdown();
}
/**
* 批量添加题目到题库(包含事务注解,仅内部使用)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void bathcAddQuestionsToBankInner(List<QuestionBankQuestion> questionBankQuestions){
try {
//批量操作,减少数据库交互次数
boolean result = this.saveBatch(questionBankQuestions);
if (!result) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "向题库添加题目失败");
}
} catch (DataIntegrityViolationException e) {
log.error("数据库唯一键冲突或违反其他完整性约束,错误信息:{}", e.getMessage());
throw new BusinessException(ErrorCode.OPERATION_ERROR, "题目已存在于该题库,无法重复添加");
} catch (DataAccessException e) {
log.error("数据库连接问题、事务问题等导致操作失败,错误信息:{}", e.getMessage());
throw new BusinessException(ErrorCode.OPERATION_ERROR, "数据库操作失败");
} catch (Exception e) {
// 捕获其他异常,做通用处理
log.error("添加题目到题库时发生未知错误,错误信息:{}", e.getMessage());
throw new BusinessException(ErrorCode.OPERATION_ERROR, "向题库添加题目失败");
}
}
3.5 批量从题库移除题目
功能介绍
该功能允许用户从题库中批量移除题目。与添加题目类似,移除操作也采用了异步处理和事务管理机制。系统会自动检查题目的存在性,并在移除后更新题库的统计信息(如题目总数)。
@Override
@Transactional(rollbackFor = Exception.class)
public void batchRemoveQuestionsFromBank(List<Long> questionIdList, Long questionBankId) {
// 参数校验
ThrowUtils.throwIf(CollUtil.isEmpty(questionIdList), ErrorCode.PARAMS_ERROR, "题目列表为空");
ThrowUtils.throwIf(questionBankId == null || questionBankId <= 0, ErrorCode.PARAMS_ERROR, "题库非法");
// 执行删除关联
for (Long questionId : questionIdList) {
// 构造查询
LambdaQueryWrapper<QuestionBankQuestion> lambdaQueryWrapper = Wrappers.lambdaQuery(QuestionBankQuestion.class)
.eq(QuestionBankQuestion::getQuestionId, questionId)
.eq(QuestionBankQuestion::getQuestionBankId, questionBankId);
boolean result = this.remove(lambdaQueryWrapper);
if (!result) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "从题库移除题目失败");
}
}
}
3.6 批量删除题目
功能介绍
该功能允许用户批量删除题目。由于删除操作可能涉及大量数据,我们采用了异步处理和事务管理机制,确保操作的高效性和一致性。系统会自动检查题目的存在性,并在删除后清理相关数据(如题目与题库的关联关系)。
@Override
@Transactional(rollbackFor = Exception.class)
public void batchDeleteQuestions(List<Long> questionIdList) {
if (CollUtil.isEmpty(questionIdList)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "要删除的题目列表为空");
}
for (Long questionId : questionIdList) {
boolean result = this.removeById(questionId);
if (!result) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "删除题目失败");
}
// 移除题目题库关系
LambdaQueryWrapper<QuestionBankQuestion> lambdaQueryWrapper = Wrappers.lambdaQuery(QuestionBankQuestion.class)
.eq(QuestionBankQuestion::getQuestionId, questionId);
result = questionBankQuestionService.remove(lambdaQueryWrapper);
if (!result) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "删除题目题库关联失败");
}
}
}
通过以上技术栈和核心功能的实现,我们的题库管理系统不仅能够高效管理大量题目,还能在高并发场景下保持稳定性和响应速度。希望本文的分享能为有类似需求的开发者提供一些参考和启发。
文章持续跟新,可以微信搜一搜公众号 [ rain雨雨编程 ],第一时间阅读,涉及数据分析,机器学习,Java编程,爬虫,实战项目等。