LeetCode评测系统又双叒叕超时了?这6个Java架构技巧让你秒建在线评测平台!
LeetCode评测系统又双叒叕超时了?这6个Java架构技巧让你秒建在线评测平台!
在线评测系统,看似就是跑个代码的事儿,但实际上技术难度堪比造火箭。今天就结合我3年在线教育平台开发经验,跟大家分享如何用Java从0到1搭建一个稳如老狗的LeetCode式评测系统!
一、在线评测系统到底是个啥?为啥这么复杂?
在线评测系统(Online Judge)的核心就是:用户提交代码,系统自动编译运行,对比输出结果,给出通过/失败的判定。
为啥在线评测系统这么复杂?
- 安全性要求极高:用户代码可能包含恶意攻击,必须完全隔离
- 性能要求苛刻:要在限定时间内完成编译、运行、评测
- 并发量巨大:成千上万用户同时提交代码
- 资源管控严格:防止恶意代码消耗过多CPU、内存
二、Java构建在线评测系统的6个核心技巧
技巧1:代码安全执行,Docker容器隔离是王道
用户提交的代码千奇百怪,安全隔离是第一要务。
@Component
public class DockerCodeExecutor {
private static final String JAVA_IMAGE = "openjdk:11-jre-slim";
private static final int TIME_LIMIT = 5000; // 5秒超时
private static final long MEMORY_LIMIT = 128 * 1024 * 1024; // 128MB
@Autowired
private DockerClient dockerClient;
/**
* 在Docker容器中执行Java代码
*/
public ExecutionResult executeJavaCode(String code, String input) {
String containerId = null;
try {
// 1. 创建临时目录和文件
String tempDir = createTempDirectory();
String javaFile = tempDir + "/Solution.java";
Files.write(Paths.get(javaFile), code.getBytes());
// 2. 创建安全的Docker容器
containerId = createSecureContainer(tempDir);
// 3. 启动容器并编译代码
dockerClient.startContainerCmd(containerId).exec();
ExecutionResult compileResult = compileJavaCode(containerId);
if (!compileResult.isSuccess()) {
return compileResult;
}
// 4. 运行Java程序
return runJavaProgram(containerId, input);
} catch (Exception e) {
log.error("代码执行异常", e);
return ExecutionResult.builder()
.status("SYSTEM_ERROR")
.error("系统错误: " + e.getMessage())
.build();
} finally {
// 清理容器资源
cleanupContainer(containerId);
}
}
/**
* 创建安全的Docker容器
*/
private String createSecureContainer(String hostPath) {
CreateContainerResponse container = dockerClient
.createContainerCmd(JAVA_IMAGE)
.withCmd("sleep", "30") // 容器存活30秒
.withWorkingDir("/app")
.withHostConfig(HostConfig.newHostConfig()
.withBinds(new Bind(hostPath, new Volume("/app")))
.withMemory(MEMORY_LIMIT) // 内存限制
.withCpuQuota(50000L) // CPU限制50%
.withNetworkMode("none") // 禁用网络
.withReadonlyRootfs(true) // 只读文件系统
.withAutoRemove(true) // 自动清理
)
.exec();
return container.getId();
}
}
技巧2:多语言支持,策略模式优雅解决
不同编程语言需要不同的处理方式,策略模式完美适配。
public interface LanguageExecutor {
ExecutionResult execute(String code, String input, int timeLimit, long memoryLimit);
String getLanguage();
}
@Component
public class JavaExecutor implements LanguageExecutor {
@Autowired
private DockerCodeExecutor dockerExecutor;
@Override
public ExecutionResult execute(String code, String input, int timeLimit, long memoryLimit) {
// 代码预处理
String processedCode = preprocessJavaCode(code);
return dockerExecutor.executeJavaCode(processedCode, input);
}
@Override
public String getLanguage() {
return "java";
}
/**
* Java代码预处理
*/
private String preprocessJavaCode(String code) {
// 添加必要的import
if (!code.contains("import java.util.*;")) {
code = "import java.util.*;\n" + code;
}
if (!code.contains("import java.io.*;")) {
code = "import java.io.*;\n" + code;
}
return code;
}
}
@Service
public class CodeExecutionService {
private final Map<String, LanguageExecutor> executorMap;
public CodeExecutionService(List<LanguageExecutor> executors) {
this.executorMap = executors.stream()
.collect(Collectors.toMap(
LanguageExecutor::getLanguage,
Function.identity()
));
}
/**
* 执行代码
*/
public ExecutionResult executeCode(SubmissionRequest request) {
LanguageExecutor executor = executorMap.get(request.getLanguage().toLowerCase());
if (executor == null) {
return ExecutionResult.builder()
.status("UNSUPPORTED_LANGUAGE")
.error("不支持的编程语言: " + request.getLanguage())
.build();
}
return executor.execute(
request.getCode(),
request.getInput(),
request.getTimeLimit(),
request.getMemoryLimit()
);
}
}
技巧3:评测核心逻辑,测试用例批量验证
评测的核心是运行测试用例,对比输出结果。
@Service
public class JudgeService {
@Autowired
private CodeExecutionService codeExecutionService;
@Autowired
private TestCaseRepository testCaseRepository;
/**
* 评测提交的代码
*/
@Async
public CompletableFuture<JudgeResult> judgeSubmission(Submission submission) {
try {
Problem problem = getProblem(submission.getProblemId());
List<TestCase> testCases = getTestCases(problem.getId());
if (testCases.isEmpty()) {
return CompletableFuture.completedFuture(
createSystemErrorResult("该题目没有测试用例")
);
}
// 执行测试用例
JudgeResult result = executeAllTestCases(submission, testCases, problem);
// 保存结果
saveJudgeResult(submission, result, testCases.size());
return CompletableFuture.completedFuture(result);
} catch (Exception e) {
log.error("评测异常: submissionId={}", submission.getId(), e);
return CompletableFuture.completedFuture(
createSystemErrorResult("系统错误: " + e.getMessage())
);
}
}
/**
* 执行所有测试用例
*/
private JudgeResult executeAllTestCases(Submission submission, List<TestCase> testCases, Problem problem) {
int passedCount = 0;
long maxExecutionTime = 0;
long maxMemoryUsage = 0;
for (int i = 0; i < testCases.size(); i++) {
TestCase testCase = testCases.get(i);
// 执行单个测试用例
ExecutionResult execResult = executeSingleTestCase(submission, testCase, problem);
// 更新统计信息
maxExecutionTime = Math.max(maxExecutionTime, execResult.getExecutionTime());
maxMemoryUsage = Math.max(maxMemoryUsage, execResult.getMemoryUsage());
// 检查执行状态
if (!"SUCCESS".equals(execResult.getStatus())) {
return createFailedResult(execResult, i + 1, maxExecutionTime, maxMemoryUsage, passedCount);
}
// 比较输出结果
if (compareOutput(execResult.getOutput(), testCase.getExpectedOutput())) {
passedCount++;
} else {
return createWrongAnswerResult(execResult, testCase, i + 1, maxExecutionTime, maxMemoryUsage, passedCount);
}
}
// 所有测试用例通过
return JudgeResult.builder()
.status("ACCEPTED")
.message("所有测试用例通过")
.executionTime(maxExecutionTime)
.memoryUsage(maxMemoryUsage)
.passedCount(passedCount)
.build();
}
/**
* 比较输出结果
*/
private boolean compareOutput(String actualOutput, String expectedOutput) {
if (actualOutput == null || expectedOutput == null) {
return Objects.equals(actualOutput, expectedOutput);
}
// 去除首尾空白字符后比较
String actual = actualOutput.trim();
String expected = expectedOutput.trim();
// 处理换行符差异
actual = actual.replaceAll("\\r\\n", "\\n");
expected = expected.replaceAll("\\r\\n", "\\n");
return actual.equals(expected);
}
}
技巧4:异步队列处理,避免评测阻塞
大量提交时,用消息队列异步处理,避免系统阻塞。
@RestController
@RequestMapping("/api/submissions")
public class SubmissionController {
@Autowired
private SubmissionService submissionService;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 提交代码
*/
@PostMapping
public ResponseEntity<SubmissionResponse> submitCode(@RequestBody SubmitRequest request) {
try {
// 1. 基础校验
validateSubmission(request);
// 2. 创建提交记录
Submission submission = createSubmission(request);
submissionService.save(submission);
// 3. 发送到评测队列
JudgeMessage message = JudgeMessage.builder()
.submissionId(submission.getId())
.problemId(submission.getProblemId())
.userId(submission.getUserId())
.language(submission.getLanguage())
.priority(calculatePriority(request))
.build();
rabbitTemplate.convertAndSend("judge.exchange", "judge.normal", message);
return ResponseEntity.ok(SubmissionResponse.builder()
.submissionId(submission.getId())
.status("PENDING")
.message("代码已提交,正在评测中...")
.build());
} catch (ValidationException e) {
return ResponseEntity.badRequest().body(
SubmissionResponse.builder()
.status("INVALID")
.message(e.getMessage())
.build()
);
}
}
}
@Component
public class JudgeQueueConsumer {
@Autowired
private JudgeService judgeService;
/**
* 处理普通优先级的评测任务
*/
@RabbitListener(queues = "judge.normal.queue")
public void handleNormalJudge(JudgeMessage message) {
processJudgeMessage(message);
}
/**
* 处理评测消息
*/
private void processJudgeMessage(JudgeMessage message) {
try {
Submission submission = submissionService.findById(message.getSubmissionId());
if (submission == null) {
log.warn("提交记录不存在: {}", message.getSubmissionId());
return;
}
// 更新状态为评测中
submission.setStatus("JUDGING");
submissionService.save(submission);
// 执行评测
CompletableFuture<JudgeResult> future = judgeService.judgeSubmission(submission);
// 等待评测完成
JudgeResult result = future.get(60, TimeUnit.SECONDS);
log.info("评测完成: submissionId={}, status={}",
submission.getId(), result.getStatus());
} catch (Exception e) {
log.error("评测异常: submissionId={}", message.getSubmissionId(), e);
updateSubmissionStatus(message.getSubmissionId(), "SYSTEM_ERROR", "系统错误");
}
}
}
技巧5:题目管理系统,支持多种题型
完善的题目管理是在线评测系统的基础。
@Entity
@Table(name = "problems")
public class Problem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "title", nullable = false)
private String title;
@Column(name = "description", columnDefinition = "TEXT")
private String description;
@Column(name = "difficulty")
@Enumerated(EnumType.STRING)
private Difficulty difficulty; // EASY, MEDIUM, HARD
@Column(name = "time_limit")
private Integer timeLimit; // 秒
@Column(name = "memory_limit")
private Long memoryLimit; // 字节
@Column(name = "sample_input", columnDefinition = "TEXT")
private String sampleInput;
@Column(name = "sample_output", columnDefinition = "TEXT")
private String sampleOutput;
@Column(name = "tags")
private String tags; // 用逗号分隔的标签
// getters and setters...
}
@Service
public class ProblemService {
@Autowired
private ProblemRepository problemRepository;
@Autowired
private TestCaseRepository testCaseRepository;
/**
* 创建题目
*/
@Transactional
public Problem createProblem(CreateProblemRequest request) {
// 1. 创建题目基本信息
Problem problem = Problem.builder()
.title(request.getTitle())
.description(request.getDescription())
.difficulty(request.getDifficulty())
.timeLimit(request.getTimeLimit())
.memoryLimit(request.getMemoryLimit())
.sampleInput(request.getSampleInput())
.sampleOutput(request.getSampleOutput())
.tags(String.join(",", request.getTags()))
.author(request.getAuthor())
.status(ProblemStatus.DRAFT)
.createTime(LocalDateTime.now())
.build();
problem = problemRepository.save(problem);
// 2. 创建测试用例
if (request.getTestCases() != null && !request.getTestCases().isEmpty()) {
createTestCases(problem.getId(), request.getTestCases());
}
return problem;
}
/**
* 获取题目列表(支持筛选和分页)
*/
public Page<ProblemListDTO> getProblems(ProblemQueryRequest request, Pageable pageable) {
Specification<Problem> spec = Specification.where(null);
// 按难度筛选
if (request.getDifficulty() != null) {
spec = spec.and((root, query, cb) ->
cb.equal(root.get("difficulty"), request.getDifficulty()));
}
// 按标题搜索
if (StringUtils.isNotBlank(request.getKeyword())) {
spec = spec.and((root, query, cb) ->
cb.like(root.get("title"), "%" + request.getKeyword() + "%"));
}
Page<Problem> problems = problemRepository.findAll(spec, pageable);
return problems.map(this::convertToProblemListDTO);
}
}
技巧6:实时状态展示,WebSocket推送结果
用WebSocket实时推送评测结果,提升用户体验。
@Component
public class JudgeResultNotifier {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 通知评测结果
*/
public void notifyJudgeResult(Long submissionId, JudgeResult result) {
try {
// 1. 构造通知消息
JudgeResultNotification notification = JudgeResultNotification.builder()
.submissionId(submissionId)
.status(result.getStatus())
.message(result.getMessage())
.executionTime(result.getExecutionTime())
.memoryUsage(result.getMemoryUsage())
.passedCount(result.getPassedCount())
.timestamp(System.currentTimeMillis())
.build();
// 2. 发送WebSocket消息给对应用户
String userKey = "submission:" + submissionId + ":user";
String userId = redisTemplate.opsForValue().get(userKey);
if (StringUtils.isNotBlank(userId)) {
messagingTemplate.convertAndSendToUser(
userId,
"/queue/judge-result",
notification
);
log.info("评测结果已推送: submissionId={}, userId={}, status={}",
submissionId, userId, result.getStatus());
}
} catch (Exception e) {
log.error("推送评测结果失败: submissionId={}", submissionId, e);
}
}
}
三、实战案例:某在线教育平台评测系统
下面分享我之前搭建的一套完整在线评测系统的关键指标。
系统架构
前端: React + Monaco Editor + WebSocket
API网关: Spring Cloud Gateway + 限流
业务服务: Spring Boot微服务集群
消息队列: RabbitMQ + Redis
评测执行: Docker容器集群
数据存储: MySQL + MongoDB + Redis
关键性能指标
- 并发处理:1万用户同时在线,500个评测任务并发
- 评测速度:Java代码平均2秒,Python代码3秒
- 系统可用性:99.9%可用率,7×24小时运行
- 资源效率:单台8核16G支持100个并发评测
四、6个避坑指南,90%的人都踩过!
1. 代码安全隔离坑
问题:用户恶意代码攻击系统
解决方案:Docker容器完全隔离,禁用网络和文件写入
2. 内存泄漏坑
问题:Docker容器没有及时清理
解决方案:设置容器自动过期,定期清理僵尸进程
3. 评测结果不准确坑
问题:输出格式差异导致误判
解决方案:智能处理空白字符,支持多格式检查
4. 高并发性能坑
问题:大量提交导致系统响应慢
解决方案:异步队列处理,优先级调度,动态扩容
5. 测试用例管理坑
问题:用例更新后历史结果不一致
解决方案:版本化管理用例,保持历史评测结果
6. 监控告警缺失坑
问题:系统异常无法及时发现
解决方案:完善监控指标,实时告警机制
五、5个核心监控指标
1. 评测成功率
- 目标值:>99%
- 告警阈值:<95%
2. 评测平均耗时
- 目标值:<3秒
- 告警阈值:>10秒
3. 系统并发能力
- 目标值:500并发
- 告警阈值:排队超过100
4. 容器资源使用率
- CPU:<70%
- 内存:<80%
- 告警阈值:>90%
5. 队列堆积情况
- 目标值:<50条
- 告警阈值:>200条
六、总结:在线评测系统的4个关键点
- 安全第一:Docker隔离用户代码,防止恶意攻击
- 性能优化:异步队列处理,合理的资源调度
- 准确评测:智能输出比对,完善的测试用例管理
- 用户体验:实时状态推送,友好的错误提示
这套在线评测系统我们已经稳定运行2年,处理了100万+次代码提交,支撑了从日活1000到10万的业务增长。记住:评测系统的核心是安全和准确,宁可慢一点,也不能出现安全漏洞和错误评测!
如果你也在搭建在线评测系统,欢迎关注公众号服务端技术精选,一起交流技术实现和避坑经验!
标题:LeetCode评测系统又双叒叕超时了?这6个Java架构技巧让你秒建在线评测平台!
作者:jiangyi
地址:http://www.jiangyi.space/articles/2025/12/21/1766304300646.html
- 一、在线评测系统到底是个啥?为啥这么复杂?
- 二、Java构建在线评测系统的6个核心技巧
- 技巧1:代码安全执行,Docker容器隔离是王道
- 技巧2:多语言支持,策略模式优雅解决
- 技巧3:评测核心逻辑,测试用例批量验证
- 技巧4:异步队列处理,避免评测阻塞
- 技巧5:题目管理系统,支持多种题型
- 技巧6:实时状态展示,WebSocket推送结果
- 三、实战案例:某在线教育平台评测系统
- 系统架构
- 关键性能指标
- 四、6个避坑指南,90%的人都踩过!
- 1. 代码安全隔离坑
- 2. 内存泄漏坑
- 3. 评测结果不准确坑
- 4. 高并发性能坑
- 5. 测试用例管理坑
- 6. 监控告警缺失坑
- 五、5个核心监控指标
- 1. 评测成功率
- 2. 评测平均耗时
- 3. 系统并发能力
- 4. 容器资源使用率
- 5. 队列堆积情况
- 六、总结:在线评测系统的4个关键点
0 评论