相信自己,終會成功
微服務代碼:? 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 是輕量級、高可用的消息中間件,適合需要可靠消息傳遞的分布式系統。通過靈活的路由規則和豐富的特性,平衡了性能與功能需求?