一、为什么平时不卡,考试最后5分钟却容易卡
假设一场考试有1000名考生,考试时长60分钟。
在前55分钟内,考生的操作通常比较分散:
有人正在阅读题目;
有人在切换题目;
有人修改答案;
有人停留思考;
有人提前交卷。
此时请求会分布在较长的时间范围内,服务器虽然持续收到访问,但整体压力相对平稳。
到了最后5分钟,考生行为开始趋同:
系统弹出剩余时间提醒;
大量考生开始检查未答题目;
频繁切换题目并修改答案;
连续触发自动保存;
部分考生主动点击交卷;
倒计时结束后,未交卷人员被统一自动交卷。
这时系统面对的不是简单的“1000人在线”,而是1000人在短时间内执行相似的高成本操作。
尤其是交卷操作,通常需要完成以下流程:
校验考试是否仍然有效;
校验考生身份和考试资格;
获取考生全部答案;
检查答案完整性;
写入最终答卷;
修改考试状态;
计算客观题成绩;
保存题目得分;
生成考试成绩;
更新参考人数、交卷人数等统计数据;
记录交卷日志和防作弊日志;
向前端返回交卷结果。
如果这些操作全部放在一个同步请求中完成,大量交卷请求同时到达后,就容易形成数据库锁等待、连接池耗尽、线程堆积和请求超时。
二、集中交卷最常见的五个性能瓶颈
1. 最后一刻才上传整张答卷
一些考试系统只在考生点击“交卷”时,才把全部答案一次性提交到服务器。
假设一张试卷有100道题,每道题还包含作答内容、题目标记、作答时间和附件信息,那么一次交卷请求的数据量可能比较大。
当大量考生同时上传完整答卷时,会同时占用:
网络带宽;
Web服务器线程;
请求解析资源;
数据库连接;
磁盘写入能力。
这种设计平时看不出问题,一旦集中交卷就会迅速放大。
2. 交卷和阅卷完全同步执行
有些系统要求交卷接口必须完成全部阅卷和统计后,才向考生返回结果。
例如,考生点击交卷后,系统同步完成:
客观题判分;
题目得分明细保存;
总分计算;
通过状态判断;
排名计算;
部门统计;
知识点正确率统计。
这些计算如果全部在交卷事务中执行,会显著拉长接口响应时间。
一个请求原本只需要200毫秒,加入阅卷和统计后可能增长到数秒。大量慢请求同时存在,很容易占满服务器工作线程。
3. 数据库事务过大
部分系统会在一个数据库事务中完成答案保存、成绩生成、试题统计、考试统计和人员档案更新。
事务越大,持有锁的时间越长。
当多名考生同时更新相同考试的统计记录,例如“已交卷人数”或“参考人数”时,还可能竞争同一行数据,形成热点更新。
4. 考生重复点击交卷
交卷后页面没有及时响应,考生通常会再次点击。
在网络抖动情况下,浏览器或网关也可能自动重试请求。
于是,同一名考生的一次交卷行为,可能对应两次、三次甚至更多次请求。
如果接口没有做好幂等控制,就可能出现:
生成两条成绩记录;
重复扣除考试次数;
重复发送证书;
多次更新统计人数;
第一次交卷成功,第二次却返回系统异常。
5. 倒计时结束后统一自动交卷
如果1000名考生的浏览器都在同一秒触发自动交卷,相当于系统主动制造了一次流量洪峰。
更危险的是,浏览器时间可能不准确,前端页面也可能已经断网或关闭。因此,不能把考试结束控制完全依赖于客户端倒计时。
三、解决集中交卷问题的核心思路
一个稳定的在线考试系统,不应把所有压力都集中到“交卷”这一个动作上。
更加合理的设计是:
作答过程中持续保存,交卷时只确认状态;系统先可靠接收,再异步完成后续处理。
整体流程可以拆分为四个阶段:
作答阶段持续保存答案;
交卷接口快速生成接收凭证;
后台异步完成阅卷和统计;
异常任务通过补偿机制恢复。
也就是说,交卷接口最重要的职责不是立即完成所有业务,而是保证:
考生的答案没有丢失;
交卷请求已经被可靠接收;
同一答卷不会重复处理;
后续任务能够继续执行。
四、第一层削峰:将答案保存分散到考试过程中
1. 自动保存不能只设置固定时间
最简单的自动保存方式,是每隔30秒保存一次答案。
但如果所有考生都在进入考试后同时开始计时,那么他们也可能在同一时间触发保存,仍然会形成周期性峰值。
更合理的方式是采用“事件触发+定时兜底+随机抖动”:
考生修改答案后,延迟几秒保存;
切换题目时保存当前题答案;
每隔一定时间执行一次兜底保存;
不同客户端增加少量随机延迟,避免请求完全同步。
例如,自动保存周期可以设置为25至35秒之间的随机时间,而不是所有人固定30秒。
2. 只保存发生变化的答案
每次自动保存时,不需要上传整张试卷。
可以为每道题维护一个版本号或修改标记,只提交发生变化的题目。
请求示例:
{ "examId": 10001, "attemptId": 820056, "clientVersion": 18, "answers": [ { "questionId": 501, "answer": "B", "answerVersion": 3 }, { "questionId": 502, "answer": "A,C", "answerVersion": 2 } ] }这样可以明显减少网络传输量和数据库写入量。
3. 服务端保存最后版本
服务端不能简单覆盖答案,而应根据版本号判断新旧。
简化SQL逻辑如下:
UPDATE exam_answer SET answer_content = ?, answer_version = ?, update_time = NOW() WHERE attempt_id = ? AND question_id = ? AND answer_version < ?;只有客户端提交的版本号大于数据库版本号时,才允许更新。
这样可以避免网络延迟导致旧答案覆盖新答案。
五、第二层削峰:交卷请求与阅卷任务解耦
1. 交卷接口只做关键操作
交卷接口应尽量缩短同步处理链路。
建议同步完成以下操作:
校验考生和答卷;
确认考试是否允许交卷;
固化最终答案版本;
将答卷状态修改为“已接收”;
生成唯一交卷凭证;
写入阅卷任务;
返回交卷已接收。
而以下操作可以异步执行:
客观题评分;
主观题待阅卷任务生成;
成绩汇总;
排名计算;
部门统计;
知识点分析;
证书生成;
个人档案更新。
考生点击交卷后,可以先看到:
“您的答卷已成功提交,系统正在处理成绩。”
而不是要求用户一直等待全部统计完成。
2. 使用消息队列削峰
当大量交卷请求进入系统后,可以先把阅卷任务写入消息队列,再由后台消费者按照系统处理能力逐步执行。
逻辑流程如下:
考生交卷 ↓ 交卷接口校验 ↓ 保存交卷凭证 ↓ 写入阅卷任务队列 ↓ 立即返回“交卷已接收” ↓ 后台异步阅卷 ↓ 生成成绩和统计数据即使短时间内产生1000个交卷任务,也不必同时启动1000次完整阅卷。
系统可以根据服务器配置启动固定数量的消费者,例如同时处理20个或50个阅卷任务,其余任务在队列中等待。
这就是典型的削峰填谷。
六、交卷幂等:重复请求只能产生一个结果
削峰解决的是“请求太多”,幂等解决的是“同一请求被处理多次”。
1. 什么是交卷幂等
交卷幂等是指:
同一名考生对同一场考试,无论点击多少次交卷,系统最终都只生成一份有效答卷和一条有效成绩。
第一次请求负责真正完成交卷。
后续重复请求不再重复执行业务,而是直接返回第一次交卷的结果。
2. 生成唯一幂等键
可以使用以下字段组成幂等键:
examId + attemptId或者:
examId + userId + examRecordId其中,attemptId应在考生进入考试时生成,并且在本次考试过程中保持不变。
例如:
SUBMIT:10001:8200563. 数据库唯一约束是最终保障
Redis分布式锁可以减少重复请求,但不能作为唯一保障。
因为Redis锁可能因超时、网络异常或服务重启而失效。
真正可靠的幂等控制,仍然要落到数据库唯一约束和条件更新上。
例如,在交卷记录表中增加唯一索引:
ALTER TABLE exam_submit_receipt ADD UNIQUE KEY uk_attempt_id (attempt_id);插入交卷凭证时:
INSERT INTO exam_submit_receipt (attempt_id, submit_no, submit_time) VALUES (?, ?, NOW());如果同一个attemptId再次插入,数据库会直接拒绝重复记录。
4. 使用状态机控制答卷流转
答卷状态不应只有“未交卷”和“已交卷”,建议设计为:
ANSWERING:作答中 SUBMITTING:交卷处理中 RECEIVED:交卷已接收 SCORING:正在阅卷 SCORED:阅卷完成 FAILED:处理异常状态变更必须按照固定方向进行,不能随意回退。
例如,交卷时使用条件更新:
UPDATE exam_attempt SET status = 'RECEIVED', submit_time = NOW() WHERE id = ? AND status IN ('ANSWERING', 'SUBMITTING');程序需要检查受影响行数。
如果受影响行数为1,说明本次请求完成了状态变更。
如果受影响行数为0,说明答卷可能已经交卷,系统应查询原交卷结果并返回,而不是再次处理。
5. Java简化示例
以下代码只用于说明幂等思路:
@Transactional public SubmitResult submitExam(Long attemptId) { ExamAttempt attempt = examAttemptRepository.findById(attemptId); if (attempt == null) { throw new BusinessException("答卷不存在"); } if (attempt.isReceivedOrScored()) { return submitReceiptRepository.findResultByAttemptId(attemptId); } int affectedRows = examAttemptRepository.markAsReceived(attemptId); if (affectedRows == 0) { return submitReceiptRepository.findResultByAttemptId(attemptId); } SubmitReceipt receipt = submitReceiptRepository.createIfAbsent(attemptId); scoringTaskRepository.createIfAbsent(attemptId); return SubmitResult.accepted(receipt.getSubmitNo()); }这里至少有三层保障:
业务状态判断;
条件更新;
数据库唯一约束。
即使两个请求几乎同时到达,也只有一个请求能够真正完成状态变更。
七、不要在交卷事务中实时更新全部统计
不少系统会在每个考生交卷时,立即执行:
UPDATE exam_statistics SET submit_count = submit_count + 1 WHERE exam_id = ?;1000名考生同时交卷,就会同时竞争同一条考试统计记录。
这种“热点行更新”很容易造成锁等待。
更加合理的方式有两种。
方案一:交卷事件异步汇总
每次交卷只记录事件,不直接修改总统计。
后台任务定期汇总:
SELECT COUNT(*) FROM exam_attempt WHERE exam_id = ? AND status IN ('RECEIVED', 'SCORING', 'SCORED');方案二:分片计数
按照考生ID或答卷ID将计数拆分到多个分片中:
exam_count_0 exam_count_1 exam_count_2 …… exam_count_15查询总数时再对多个分片求和。
对于一般企业考试系统,异步汇总通常已经能够满足需求,结构也更加简单。
八、自动交卷不能完全依赖浏览器
考试倒计时结束后,浏览器可以发起自动交卷,但服务端必须有兜底机制。
原因包括:
考生电脑断网;
浏览器被关闭;
页面脚本停止运行;
客户端时间被修改;
自动交卷请求超时。
因此,考试结束时间必须以服务端时间为准。
后台可以定时扫描已经超过截止时间、但仍处于作答状态的答卷,并将其加入自动交卷任务。
例如:
SELECT id FROM exam_attempt WHERE status = 'ANSWERING' AND exam_end_time < NOW() LIMIT 500;扫描任务不应一次性处理全部过期答卷,而应分批读取、分批提交,避免后台任务自己制造新的流量峰值。
九、以宏远培训考试系统为例,集中交卷如何处理
宏远培训考试系统主要面向企业、集团单位、政府机构和行业培训考核场景。这类考试通常具有人员集中、考试时间统一、成绩要求准确、过程需要留痕等特点。
从宏远培训考试系统面向集中考试的设计思路来看,解决最后阶段卡顿问题,并不是单纯提高服务器配置,而是将风险分散到考试全过程。
1. 作答过程中持续保存
考生答题时,系统会持续记录作答内容,减少最后交卷时一次性上传整张答卷的压力。
即使考生在考试过程中出现页面刷新、短时断网或浏览器异常,已经成功保存的答案仍然可以作为恢复基础。
2. 交卷动作与答案保存分离
考生点击交卷时,系统重点确认当前答卷的最终状态,而不是从零开始重新接收所有答题数据。
这种设计可以缩短交卷接口的处理时间,避免大量完整答卷同时上传。
3. 防止重复提交
针对考生重复点击交卷、网络重试和页面响应延迟等情况,宏远培训考试系统会以考生考试记录作为唯一处理依据。
同一次考试记录只能形成一个有效交卷结果。
后续重复请求返回已有处理状态,不重复生成成绩,也不会重复增加交卷人数。
4. 阅卷与统计分阶段处理
客观题阅卷、成绩汇总、排名统计、部门对比和数据驾驶舱更新,不必全部阻塞在考生交卷页面中。
系统可以先确认答卷接收成功,再完成成绩和统计处理,减少集中交卷时的同步计算压力。
5. 服务端自动交卷兜底
对于考试时间结束后仍未主动交卷的人员,系统根据服务端考试时间执行自动交卷,避免完全依赖考生浏览器。
同时,自动交卷任务可以分批处理,而不是在截止时间到达的一瞬间对所有人员同时执行完整阅卷。
6. 交卷过程全程留痕
系统会记录考生交卷时间、考试状态、答案保存情况和相关操作日志。
在出现网络异常或考生对交卷结果存在疑问时,管理员可以根据日志判断:
考生最后一次答案保存时间;
交卷请求是否到达服务器;
系统是否已经生成交卷凭证;
阅卷任务是否执行完成;
是否发生重复提交。
对于正式考试来说,这种可追溯能力与性能优化同样重要。
十、服务器扩容仍然重要,但不能替代架构设计
削峰和幂等设计并不意味着服务器配置不重要。
服务器仍然需要根据考试规模配置:
CPU核心数;
内存容量;
数据库连接数;
磁盘IOPS;
网络带宽;
应用实例数量;
消息队列消费者数量。
但服务器扩容解决的是容量问题,削峰和幂等解决的是流量模型和业务正确性问题。
如果系统仍然采用“最后一次上传全部答案+同步阅卷+同步统计+无幂等控制”的方式,即使服务器配置提高一倍,也可能只是在推迟故障出现的时间。
十一、集中考试前应该怎样压测
测试在线考试系统,不能只测试登录人数。
更重要的是模拟真实的最后5分钟。
建议至少设计以下压测场景:
场景一:持续自动保存
模拟1000名考生每隔25至35秒随机保存部分答案。
观察:
接口平均响应时间;
95%响应时间;
数据库写入速度;
连接池使用率;
错误率。
场景二:集中主动交卷
在1分钟内逐步发起1000次交卷请求,而不是平均分布在整场考试中。
观察:
交卷接收成功率;
重复提交处理结果;
消息积压数量;
数据库锁等待;
线程池队列长度。
场景三:同一考生重复提交
针对同一个attemptId并发发起5至10次交卷请求。
正确结果应当是:
只生成一条交卷记录;
只生成一条成绩记录;
所有请求最终返回同一个交卷状态。
场景四:服务异常恢复
在阅卷过程中模拟服务重启或消费者异常。
系统恢复后,应当能够继续处理未完成任务,而不是出现答卷已经提交但永远没有成绩的情况。
十二、上线验收不能只看“能不能交卷”
一套在线考试系统是否稳定,至少应检查以下内容:
答案是否持续自动保存;
页面刷新后答案能否恢复;
断网恢复后能否继续作答;
重复点击交卷是否只产生一个结果;
交卷接口超时后是否能够查询原结果;
考试结束后是否有服务端自动交卷;
阅卷任务失败后是否能够重试;
是否存在重复成绩;
是否存在交卷人数重复累计;
集中交卷时数据库是否出现长时间锁等待;
成绩统计是否与实际答卷一致;
交卷和异常过程是否有日志可追溯。
只有登录、答题和单人交卷正常,并不能证明系统可以支撑正式集中考试。
十三、总结
考试最后5分钟容易卡,并不是因为考生突然变多,而是因为大量考生在相近时间执行了自动保存、答案修改、主动交卷、自动交卷、阅卷和统计等高成本操作。
真正有效的解决方案,应当包括:
作答过程中持续保存答案;
只上传发生变化的数据;
交卷接口轻量化;
交卷与阅卷异步解耦;
使用消息队列削峰;
使用状态机控制答卷流程;
通过条件更新和唯一索引实现幂等;
避免交卷事务中更新热点统计数据;
由服务端完成超时自动交卷;
通过补偿任务处理异常状态。
以宏远培训考试系统的集中考试设计思路为例,稳定性并不依赖某一个技术点,而是通过答案保存、交卷确认、重复提交控制、异步阅卷、自动交卷和日志追踪形成完整保障链路。
对于企业正式考试来说,系统不仅要保证“多数人能够交卷”,更要保证每一名考生的答案都能保存、每一次交卷都能确认、每一条成绩都准确且可追溯。