学生在线考试系统需从数据边界与并发风险倒推设计:拆分exam_paper、exam_question、exam_attempt三表隔离过程与结果;交卷用ON DUPLICATE KEY UPDATE写入+异步判卷;通过Redis exam_session_token管控答题状态生命周期;须定期校验主从数据一致性。

学生在线考试系统不是“先搭框架再填业务”,而是得从数据边界和并发风险倒推设计——比如一次考试可能有 2000 人同时交卷,UPDATE exam_record SET score = ?, status = 'submitted' WHERE id = ? 这条语句若没加 SELECT FOR UPDATE 或没走唯一索引定位,就容易丢分或重复判卷。
数据库表结构必须隔离「考试过程」和「考试结果」
很多初学者把所有字段堆在一张 exam_record 表里,导致事务臃肿、查询慢、历史数据难归档。实际应拆成三张核心表:
-
exam_paper:存试卷元信息(标题、总分、时限、是否启用),不存题目内容 -
exam_question:存题目(题干、类型、分值、正确答案),用paper_id关联,支持一卷多题 -
exam_attempt:每次考生作答的快照(含question_id、student_answer、is_correct、submit_time),主键为(attempt_id, question_id)复合唯一,防重复提交
关键点:exam_attempt 表必须设 ON DELETE CASCADE 到 exam_paper,否则删试卷时残留作答数据会污染统计。
Spring Boot 中控制交卷逻辑不能只靠 @Transactional
一个典型的错误是给整个 submitExam() 方法加 @Transactional,然后在里面查题、判分、更新成绩、发消息——这会导致事务时间过长,连接池打满。正确做法是分阶段:
立即学习“Java免费学习笔记(深入)”;
- 第一步:用
INSERT INTO exam_attempt ... ON DUPLICATE KEY UPDATE原子写入每道题答案(依赖(attempt_id, question_id)唯一索引) - 第二步:异步触发判卷任务(用
@Async+ 独立线程池),查exam_attempt中status = 'pending'的记录批量判分 - 第三步:判完后用单条
UPDATE exam_record SET score = ?, status = 'graded' WHERE id = ? AND status = 'submitted'更新主记录,带条件避免覆盖重判
注意:@Async 方法不能是 private,且需在启动类加 @EnableAsync;若用 RabbitMQ 替代异步,消息体里必须带 attempt_id 和 version 防重放。
前端防作弊的关键不是加密,而是控制「答题状态生命周期」
单纯对 questionId 或 answer 做前端加密毫无意义,真正有效的是服务端严格管理状态流转:
- 考生进入考试页时,后端生成带过期时间(如 15 分钟)的
exam_session_token,存入 Redis,key 为session:{studentId}:{paperId} - 每次 AJAX 提交答案前,前端必须附带该 token;后端校验 token 存在且未过期,再查
exam_session_token对应的remaining_time字段是否 > 0 - 交卷接口必须校验
status IN ('in_progress', 'paused'),且更新时用UPDATE ... SET status = 'submitted' WHERE status = 'in_progress',避免已交卷用户二次提交
别忽略:浏览器刷新会丢失前端计时,所以倒计时必须由后端通过 WebSocket 或轮询推送 remaining_time,而不是只信前端 JS 的 setInterval。
最常被跳过的一步是「考试结束后的数据一致性检查」:比如某次数据库主从延迟导致部分 exam_attempt 写入从库失败,但主库已返回成功。上线后必须定期跑脚本比对 exam_record.score 和对应 exam_attempt 的 sum,差值非零即要告警——这个校验逻辑不能等出问题才补。










