微服务项目->在线oj系统(Java版 - 5)
相信自己,终会成功
微服务代码: lyyy-oj: 微服务
目录
C端代码
用户题目接口
修改后用户提交代码(应用版)
用户提交题目判题结果
代码沙箱
1. 代码沙箱的核心功能
2. 常见的代码沙箱实现方式
3. 代码沙箱的关键问题与解决方案
4. 你的代码如何与沙箱交互?
6. 总结
Elasticsearch
RabbitMQ
RabbitMQ 主要功能
RabbitMQ 工作流程示例
典型应用场景
C端代码
用户题目接口
从前端接收到数据,判断是什么语言,如果是Java语言(目前只能进行Java语言的判定)
利用代码沙箱(下方有介绍),对语言进行判断
assembleJudgeSubmitDTO:拿到questionId , 从es中查询题目信息
如果questionES不等于空,
BeanUtil.copyProperties(questionES,judgeSubmitDTO),将questionES复制给 judgeSubmitDTO
否则,从数据库中查找数据,将查出的数据复制给judgeSubmitDTO
然后将数据存入es中
将从线程池中拿到的数据赋值给judgeSubmitDTO
拼接代码后进行解析测试用例
步骤 | 说明 |
---|---|
questionCaseList.stream() | 将List<QuestionCase> 转为Stream<QuestionCase> ,支持链式操作 |
.map(QuestionCase::getInput) | 提取每个QuestionCase 对象的input 字段(方法引用,等价于x -> x.getInput() ) |
.toList() | 将Stream<String> 收集为不可变的List<String> (Java 16+特性) |
setInputList/setOutputList | 最终将输入/输出列表设置到判题DTO对象 |
数据流转示例
假设原始数据:
List<QuestionCase> questionCaseList = [{"input": "1 2", "output": "3"},{"input": "3 4", "output": "7"}
]
转换后结果:
inputList = ["1 2", "3 4"] // 所有input的集合
outputList = ["3", "7"] // 所有output的集合
@Override//后端接收到请求,获取参数,根据getProgramType判断用户提交代码语言类型// UserSubmitDTO(用户提交的数据,包括题目ID、代码、考试ID等)
// JudgeSubmitDTO(判题服务需要的数据,包括题目信息、测试用例、用户代码等)public R<UserQuestionResultVO> submit(UserSubmitDTO submitDTO) {Integer programType = submitDTO.getProgramType();if(ProgramType.JAVA.getValue().equals(programType)){//按照Java逻辑处理JudgeSubmitDTO judgeSubmitDTO=assembleJudgeSubmitDTO(submitDTO);
// remoteJudgeService.doJudgeJavaCode(judgeSubmitDTO)return remoteJudgeService.doJudgeJavaCode(judgeSubmitDTO);}throw new ServiceException(ResultCode.FAILED_NOT_SUPPORT_PROGRAM);}private JudgeSubmitDTO assembleJudgeSubmitDTO(UserSubmitDTO submitDTO) {Long questionId = submitDTO.getQuestionId();//orElse,findbyId返回的是Optional对象,并不是数据本身,如果能查出数据,返回数据本身//查不出来数据,返回null// 1. 查询题目信息(优先ES,不存在则查MySQL并缓存)QuestionES questionES = questionRepository.findById(questionId).orElse(null);JudgeSubmitDTO judgeSubmitDTO=new JudgeSubmitDTO();if(questionES!=null){BeanUtil.copyProperties(questionES,judgeSubmitDTO);}else{Question question = questionMapper.selectById(questionId);BeanUtil.copyProperties(question,judgeSubmitDTO);questionES=new QuestionES();// 2. 组装 JudgeSubmitDTOBeanUtil.copyProperties(question, questionES);questionRepository.save(questionES);}// 3. 设置用户信息(从 ThreadLocal 获取用户ID)judgeSubmitDTO.setUserId(ThreadLocalUtil.get(Constants.USER_ID,Long.class));judgeSubmitDTO.setExamId(submitDTO.getExamId());judgeSubmitDTO.setProgramType(submitDTO.getProgramType());// 4.拼接用户代码和题目主函数judgeSubmitDTO.setUserCode(codeConnect(submitDTO.getUserCode(),questionES.getMainFuc()));// 5. 解析测试用例(从 ES 的 JSON 字符串转换成 List)List<QuestionCase> questionCaseList = JSONUtil.toList(questionES.getQuestionCase(), QuestionCase.class);List<String> inputList = questionCaseList.stream().map(QuestionCase::getInput).toList();judgeSubmitDTO.setInputList(inputList);List<String> outputList = questionCaseList.stream().map(QuestionCase::getOutput).toList();judgeSubmitDTO.setOutputList(outputList);return judgeSubmitDTO;}
修改后用户提交代码(应用版)
JudgeProducer(使用了RabbitMQ(下方有介绍))
/*** 将用户提交的代码通过RabbitMQ异步发送给判题服务(目前仅支持Java)* @param submitDTO 用户提交的代码信息,包含代码内容、题目ID、编程语言类型等* @return true 提交成功 | 抛出异常 提交失败(不支持的编程语言)* @throws ServiceException 如果编程语言不支持,抛出业务异常(ResultCode.FAILED_NOT_SUPPORT_PROGRAM)*/@Overridepublic boolean rabbitSubmit(UserSubmitDTO submitDTO) {// 1. 获取用户提交的编程语言类型Integer programType = submitDTO.getProgramType();if(ProgramType.JAVA.getValue().equals(programType)){//按照Java逻辑处理, 组装判题服务需要的DTO(包括代码、测试用例等信息)JudgeSubmitDTO judgeSubmitDTO=assembleJudgeSubmitDTO(submitDTO);// 通过RabbitMQ生产者将判题任务发送到消息队列(异步处理)
// 把参数给rabbitmq,但是没执行判题结果,目前实现是同步调用远程服务judgeProducer.produceMsg(judgeSubmitDTO);// 返回true表示消息已成功提交到队列(注意:不表示判题已完成)return true;}throw new ServiceException(ResultCode.FAILED_NOT_SUPPORT_PROGRAM);}
@Component
//@Component:将该类标记为 Spring 组件,由 Spring 容器管理
@Slf4j
public class JudgeProducer {@Autowiredprivate RabbitTemplate rabbitTemplate;public void produceMsg(JudgeSubmitDTO judgeSubmitDTO) {try {
// 使用 RabbitTemplate 向 RabbitMQ
// 发送消息消息内容是 JudgeSubmitDTO(判题提交数据传输对象)
// 发送到名为 OJ_WORK_QUEUE 的队列rabbitTemplate.convertAndSend(RabbitMQConstants.OJ_WORK_QUEUE, judgeSubmitDTO);} catch (Exception e) {log.error("生产者发送消息异常", e);throw new ServiceException(ResultCode.FAILED_RABBIT_PRODUCE);}}
}
用户提交题目判题结果
/*** 根据考试ID、题目ID和时间戳查询用户的判题结果* @param examId 考试ID* @param questionId 题目ID* @param currentTime 提交时间标识(用于区分同一题目的多次提交)* @return UserQuestionResultVO 包含判题状态、执行结果、用例详情等*/@Overridepublic UserQuestionResultVO exeResult(Long examId, Long questionId, String currentTime) {//把结果获取出来// 1. 从ThreadLocal中获取当前用户ID(基于登录上下文)Long userId = ThreadLocalUtil.get(Constants.USER_ID, Long.class);// 2. 查询数据库获取用户提交记录UserSubmit userSubmit = userSubmitMapper.selectCurrentUserSubmit(userId, questionId, examId, currentTime);// 3. 构建返回VO对象UserQuestionResultVO resultVO = new UserQuestionResultVO();// 4. 判题结果不存在的情况(可能还在判题中)if (userSubmit == null) {resultVO.setPass(QuestionResType.IN_JUDGE.getValue()); // 设置状态为"判题中"}// 5. 存在判题结果else {resultVO.setPass(userSubmit.getPass());resultVO.setExeMessage(userSubmit.getExeMessage());if (StrUtil.isNotEmpty(userSubmit.getCaseJudgeRes())) {resultVO.setUserExeResultList(JSON.parseArray(userSubmit.getCaseJudgeRes(), UserExeResult.class));}}return resultVO;}
代码沙箱
代码沙箱(Code Sandbox)是一种安全隔离的执行环境,用于运行不受信任的代码(如用户提交的编程题答案),防止恶意代码影响主系统。在在线判题系统(Online Judge)中,代码沙箱是核心组件之一。
1. 代码沙箱的核心功能
功能 | 说明 |
安全隔离 | 防止用户代码破坏主机(如删除文件、无限循环、占用资源)。 |
资源限制 | 限制 CPU、内存、执行时间,避免恶意代码耗尽系统资源。 |
输入/输出控制 | 提供标准输入(测试用例),捕获标准输出/错误,与判题系统交互。 |
多语言支持 | 支持 Java、Python、C++ 等语言的编译和运行。 |
错误处理 | 捕获运行时异常、编译错误,并返回友好提示。 |
2. 常见的代码沙箱实现方式
1.基于 Docker 的沙箱
原理:每个用户提交的代码在一个临时 Docker 容器中运行,运行后销毁。
优点:
强隔离性(进程、文件系统、网络均隔离)。
可限制 CPU、内存等资源(通过 cgroups)。
示例流程:
用户提交代码 → 判题系统接收。
生成临时 Docker 容器,挂载代码文件。
在容器内编译/运行代码,传入测试用例。
捕获输出,对比预期结果。
销毁容器。
2. 基于 JVM 沙箱(Java 专用)
原理:利用 Java 的 SecurityManager 或字节码修改(如 ASM)限制敏感操作。
优点:
轻量级,启动快。
适合纯 Java 判题场景。
缺点:
无法完全隔离系统调用(如 System.exit())。
需要自定义安全策略。
3. 第三方沙箱服务
示例:
Judge0:开源的在线判题沙箱(支持 60+ 语言)。
Piston:轻量级多语言执行引擎。
优点:无需自行维护沙箱环境。
3. 代码沙箱的关键问题与解决方案
问题 | 解决方案 |
恶意代码 | 使用 Docker 隔离,限制系统调用(如 fork、exec)。 |
无限循环 | 设置超时机制(如 Linux 的 timeout 命令)。 |
内存溢出 | 通过 -Xmx 限制 JVM 内存,或 Docker --memory 限制容器内存。 |
文件系统安全 | Docker 使用只读文件系统,或临时挂载空目录。 |
网络隔离 | 禁用容器网络(--network none)。 |
4. 你的代码如何与沙箱交互?
执行流程:
用户提交代码 → 你的服务组装 JudgeSubmitDTO(题目ID、代码、测试用例等)。
通过 Feign 调用判题服务(remoteJudgeService.doJudgeJavaCode)。
判题服务将代码发送到 代码沙箱 执行。
沙箱返回结果(通过/失败、错误信息、用时等)。
你的服务接收结果并返回给用户。
6. 总结
代码沙箱 是判题系统的核心,确保安全性和稳定性。
推荐方案:
小型系统:用 Docker 快速实现。
大型系统:结合 Kubernetes 管理沙箱集群。
扩展方向:
支持更多语言(Python、C++)。
分布式判题(提高并发能力)。
为什么需要沙箱?
安全隔离:防止用户代码破坏宿主系统。
资源控制:限制CPU/内存使用,避免恶意代码耗尽资源。
环境一致性:确保每次执行都在干净的环境中运行。
Elasticsearch
官方网站:
Elastic Docs | Elastic
Elasticsearch(简称 ES)是一个开源的分布式 搜索和分析引擎,基于 Apache Lucene 构建,专为处理海量数据设计,支持近实时(NRT, Near Real-Time)搜索。
优点
高性能搜索,支持复杂查询(全文检索、模糊匹配、聚合分析)。
水平扩展能力强,适合大数据场景。
生态完善(ELK Stack、APM、SIEM 等)。
缺点
不支持事务(不适合金融级一致性要求场景)。
资源消耗较高(尤其是内存)。
学习曲线较陡(需理解分词、映射、集群管理等)
RabbitMQ
RabbitMQ 是一个开源的 消息代理(Message Broker),实现了 AMQP(Advanced Message Queuing Protocol) 协议,用于在分布式系统中存储、转发消息。
核心角色:生产者(Producer)→ RabbitMQ → 消费者(Consumer)
典型场景:异步任务处理、应用解耦、流量削峰、分布式系统通信。
概念 | 说明 |
---|---|
Producer | 消息生产者,发送消息到 Exchange |
Consumer | 消息消费者,从 Queue 接收消息 |
Exchange | 消息路由组件,决定消息投递到哪些 Queue(类型:Direct、Fanout、Topic、Headers) |
Queue | 存储消息的缓冲区,消费者从中订阅消息 |
Binding | Exchange 和 Queue 的绑定规则(如路由键 Routing Key) |
Channel | 轻量级连接(复用 TCP 连接,减少开销) |
Virtual Host | 虚拟隔离环境(类似命名空间,不同 vhost 资源互不干扰) |
RabbitMQ 主要功能
消息路由(Exchange Types)
Direct Exchange
→ 精确匹配 Routing Key
,消息投递到完全匹配的 Queue。
// 示例:日志级别路由(error、warning、info)
channel.queueBind("error_queue", "logs_exchange", "error");
Fanout Exchange
→ 广播模式,消息发送到所有绑定的 Queue(忽略 Routing Key)。
// 示例:新闻通知广播
channel.exchangeDeclare("news", BuiltinExchangeType.FANOUT);
Topic Exchange
→ 通配符匹配 Routing Key
(*
匹配一个词,#
匹配多个词)。
// 示例:订单路由(order.create、order.payment.success)
channel.queueBind("queue_payment", "orders", "order.payment.*");
Headers Exchange
→ 基于消息头(Headers)匹配,不依赖 Routing Key(性能较低,较少使用)。
消息可靠性
消息确认(ACK/NACK)
→ 消费者处理成功后发送 ACK
,失败时 NACK
(可配置重试或进入死信队列)。
channel.basicConsume(queue, false, consumer); // 手动ACK
channel.basicAck(deliveryTag, false); // 确认处理成功
持久化(Persistence)
→ Exchange、Queue、消息均可持久化到磁盘,防止服务重启丢失。
// 声明持久化队列
channel.queueDeclare("task_queue", true, false, false, null);
死信队列(DLX)
→ 处理失败或超时的消息可转发到死信队列,用于异常监控和重试。
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "dlx_exchange");
channel.queueDeclare("normal_queue", false, false, false, args);
高级特性
TTL(Time-To-Live)
→ 设置消息或队列的过期时间(超时未消费则自动删除)。
// 消息级别TTL
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder().expiration("60000") // 60秒过期.build();
channel.basicPublish(exchange, routingKey, props, message.getBytes());
优先级队列(Priority Queue)
→ 消息按优先级消费(需队列声明时支持)。
Map<String, Object> args = new HashMap<>();
args.put("x-max-priority", 10); // 最大优先级为10
channel.queueDeclare("priority_queue", false, false, false, args);
集群与镜像队列
→ 支持多节点集群,镜像队列(Mirrored Queue)实现高可用。
RabbitMQ 工作流程示例
典型应用场景
-
异步任务处理
→ 用户注册后异步发送邮件/短信。 -
应用解耦
→ 订单系统与库存系统通过消息队列通信。 -
流量削峰
→ 秒杀请求先写入队列,后端按能力消费。 -
日志收集
→ 多个服务发送日志到统一队列,由消费者存储到ES/数据库。
对比其他消息队列
RabbitMQ | Kafka | RocketMQ | |
---|---|---|---|
协议 | AMQP | 自定义协议 | 自定义协议 |
吞吐量 | 中等(万级TPS) | 高(百万级TPS) | 高(十万级TPS) |
延迟 | 低(毫秒级) | 中(依赖批量) | 低 |
适用场景 | 业务消息、实时处理 | 日志流、大数据 | 金融级事务消息 |
RabbitMQ 是轻量级、高可用的消息中间件,适合需要可靠消息传递的分布式系统。通过灵活的路由规则和丰富的特性,平衡了性能与功能需求