毕业设计实战从零构建一个高可用的刷题平台后端架构摘要许多学生在毕业毕业设计实战从零构建一个高可用的刷题平台后端架构摘要许多学生在毕业设计中选择开发刷题平台却常因缺乏工程经验而陷入性能瓶颈、接口混乱或数据一致性问题。本文基于真实毕业设计场景详解如何使用 Spring Boot MyBatis Plus Redis 构建具备题目管理、用户提交、判题回调等核心功能的后端系统。通过引入消息队列解耦判题服务、利用 Redis 缓存热点题目、设计幂等性提交接口显著提升系统吞吐量与稳定性。读者将获得一套可直接复用的模块化代码结构与部署 checklist。1. 背景痛点学生项目常见“三座大山”毕业设计里做“刷题平台”听起来简单落地时却常被以下问题卡住判题阻塞同步判题导致线程长时间挂起并发一上来整站 504。重复提交前端连点两下“提交”数据库里出现两条记录用户一脸懵。冷启动延迟题目列表接口每次全表扫描首页打开 3 s 起步答辩现场直接翻车。这些痛点本质上是“学生项目”与“工程系统”之间的鸿沟功能代码能跑但缺容错、缺横向扩展、缺观测手段。下文用一套最小可用、却可线性扩展的架构带你把“玩具”升级成“产品”。2. 技术选型为什么不是 Django也不是本地内存维度Spring BootDjango/Flask结论依赖注入与 AOP原生支持靠第三方Spring 生态对事务、幂等、重试的封装更成熟横向扩展无状态 Jar 任意注册中心Python GIL 限制多进程利用率Java 多线程模型更适合 CPU 密集判题社区组件MyBatis Plus、Spring Cloud、RocketMQ相对分散企业级方案直接搬来即用缓存方案对比本地内存进程重启即失效多实例时缓存漂移无法横向扩展。Redis独立进程可集群支持 TTL、LRU、Pub/Sub天然适合“热点题目”与“判题结果”缓存。综上后端主栈锁定Spring Boot 2.7 MyBatis Plus Redis 6.x RocketMQ 4.9部署在 2C4G 单机上即可抗住毕业设计答辩并发。3. 模块划分与核心实现系统分三层网关层Nginx、业务层Spring Boot、判题层Sandbox。本文聚焦业务层内部再拆为题目服务Problem Service提交服务Submit Service判题回调Judge Callback3.1 题目服务缓存 分页 索引热点题目近 7 日提交量 Top 200在 Redis 采用hash结构缓存字段即题号值序列化为 JSON冷数据走 DB分页用 MyBatis Plus 的Page对象。缓存穿透用布隆过滤器拦截缓存雪崩加随机 60–120 s 的 TTL jitter。3.2 提交服务接口幂等性设计前端提交时携带client_submit_idUUID后端用数据库唯一索引实现幂等UNIQUE KEY uk_user_submit (user_id, client_submit_id)重复请求直接返回原结果避免重复入库。核心代码见第 4 节。3.3 判题回调消息队列解耦提交服务只负责“写记录 发消息”不等待判题结果Sandbox 判完后向 MQ 发送JudgeFinishedEvent业务层消费后更新状态。事件体例如下{ submitId: 142857, result: AC, time: 120, memory: 65536 }消费端幂等利用submitId做幂等键更新前判断状态是否已终态AC/WA/TLE 等防止重复累加通过数。4. 关键代码片段含注释4.1 SubmitController——接收提交、幂等保护RestController RequestMapping(/api/submit) RequiredArgsConstructor public class SubmitController { private final SubmitService submitService; /** * 1. 幂等键clientSubmitId * 2. 事务边界仅落库与发消息不等待判题 */ PostMapping public ApiResultSubmitDTO submit(LoginUser Long userId, Valid RequestBody SubmitRequest req) { // 重复提交直接返回 SubmitDTO exist submitService.getByUserAndClientId(userId, req.getClientSubmitId()); if (exist ! null) { return ApiResult.success(exist); } // 新提交本地事务 写库 发 MQ SubmitDTO dto submitService.doSubmit(userId, req); return ApiResult.success(dto); } }4.2 JudgeEventConsumer——消费判题结果保证幂等Component RocketMQConsumer(topics topic_judge_result) Slf4j RequiredArgsConstructor public class JudgeEventConsumer { private final SubmitService submitService; private final RedisTemplateString, String redisTemplate; Override public void onMessage(JudgeFinishedEvent event) { Long submitId event.getSubmitId(); String key judge:result: submitId; // 1. 利用 Redis setnx 做分布式锁防并发重复消费 Boolean absent redisTemplate.opsForValue().setIfAbsent(key, 1, Duration.ofMinutes(5)); if (Boolean.FALSE.equals(absent)) { log.warn(duplicate consume, submitId{}, submitId); return; } // 2. 更新库版本号乐观锁兜底 boolean updated submitService.updateResult(event); if (!updated) { log.error(update submit result failed, event{}, event); } } }5. 性能与安全并发、防刷、SQL 注入竞争条件更新提交状态时使用乐观锁version字段CAS 失败重试 3 次仍失败则日志告警人工介入。防刷机制接口限流基于 Redis 的令牌桶每用户 10 次/60 s。验证码同一 IP 5 min 内提交超过 20 次弹出图形验证码。代码相似度检测引入sim命令重复率 90 % 直接判 0 分并记录。SQL 注入MyBatis Plus 内置#{}预编译杜绝拼接动态排序用Wrapper的orderBy方法内部白名单校验列名。6. 生产级避坑 checklist坑点现象解决索引缺失题目列表按difficulty create_time查询 2 s联合索引(difficulty, create_time)后降至 20 ms判题超时无重试Sandbox 宕机消息消费成功但结果丢失消费端 ack 前检查返回码非 200 抛异常MQ 自动重试 16 次日志缺失线上出错无法复现接入traceId透传Controller、MQ、线程池统一 MDC 打印Redis 大 Key缓存整表select *导致 value 5 MB网卡打满拆分为hash分片只缓存必要字段大事务提交接口里同步调用判题 写库 更新通过数锁等待 3 s拆分为“写提交记录”与“更新通过数”两个事务后者异步7. 部署与可观测CI 脚本mvn -T 1C clean package -Dmaven.test.skiptrue打出 fat-jar配合systemd托管。Dockerfile仅 30 行基于openjdk:17-jre-slim layers 缓存缓存依赖。Prometheus GrafanaJVM 级GC、线程数、内存应用级QPS、RT、提交成功率业务级7 日 AC 率、题目冷热分布。告警RT 1 s 持续 2 min 或错误率 5 % 即刻飞书群机器人推送。8. 后续思考如何支持多语言判题沙箱当前 Sandbox 只支持 C/C如果后续想扩展 Java、Python、Go需要镜像隔离用runsc或kata-containers替代裸docker run防止ptrace逃逸。资源配额CPU、内存、seccomp 统一配置不同语言复用同一套 cgroup 模板。MQ 路由根据语言类型投递到不同 topic消费端水平扩容互不干扰。结果归一化统一返回cpu_time、memory、exit_code业务层零改动。整套代码已开源在 GitHub欢迎 fork 并提交 Pull Request一起把毕业设计项目做成能写进简历的工业级作品。