ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java+MySQL构建可扩展在线评测系统:从判题沙箱到数据库设计

Java+MySQL构建可扩展在线评测系统:从判题沙箱到数据库设计 简介基于JavaMySQL实现的Web程序在线评测系统课程设计资源包面向需要学习Spring、Hibernate、Lucene等主流框架整合开发的Java学习者也可作为毕业设计或课设项目的参考蓝本。系统覆盖用户选题解答、在线评测、统计信息查看并完整支持教师建课布题、布置作业学生选课提交作业以及内部论坛讨论等教学闭环模块划分清晰便于按需扩展与二次开发。压缩包共872个文件大小约21.11MB文件类型以js、java、class、xml、jsp、css为主分别对应前端交互脚本、后端业务代码、编译产物、ORM映射与页面模板另含PDF说明文档和设计模型文件可帮助理解从架构设计到部署运行的全过程。已有204人学习下载适合希望快速搭建可运行OJ系统并深入理解Java Web分层开发的读者。1. 程序在线评测系统为什么说“可扩展”比“能跑通”更重要如果你要做一个基于JavaMySQL实现Web可扩展的程序在线评测系统最容易被低估的其实是“判题”这两个字。你很快会发现把题目和提交记录存进数据库很简单真正让人睡不着的是用户代码怎么安全地编译运行、怎么限制它不把服务器拖垮、怎么给出不冤枉人的判定结果。这个标题里的“可扩展”也不是口号题库会加题型比赛会加语言参赛规模会翻倍表结构和判题流程一开始没留扩展点后面每次加功能都要动老代码。这套系统能解决教学OJ、竞赛平台、企业内部coding测验的完整闭环适合正在做毕业设计、想给社团搭比赛平台或者要把在线编程考试落到内网环境的人。先把“判题”这条主链路想清楚剩下的功能都是围绕它长出来的叶子。2. 从提交到出分可扩展 OJ 的整体架构与选型理由2.1 一次提交的七个环节从题目到出分的链路抛开花哨的界面OJ 系统的核心链路可以拆成七个环节题目与测试用例管理、用户提交代码、编译或解释执行、沙箱运行、输出比对、判题结果回写、结果展示。这七个环节里真正决定系统天花板的是第 3 到第 6 步。很多项目挂在“能提交、能出分”的假象上用户代码一多或者测试数据一大就开始超时、卡死、误判。把每个环节单独建模是我做这套系统时坚持的第一原则。题目管理负责把题目、测试用例、判定规则存进 MySQL提交服务只做一件事——接收代码、生成 submission 记录、放进待判队列编译阶段把用户代码变成可执行文件沙箱阶段负责限制资源输出比对根据题型决定是精确匹配还是走 Special Judge回写阶段把结果集中更新回数据库最后前端展示。每个环节都留接口后面加题型、加语言、加比赛模式才不用推倒重来。扩展点具体落在哪里用下面这个表可以看得很清楚环节技术承载扩展方向题目管理MySQL 表 JSON 配置字段新题型、Special Judge、数据分组提交服务Java Web Controller Service多语言、模板代码、代码查重编译Java ProcessBuilder 调编译器新增语言只需注册新编译命令沙箱运行独立进程 资源限制更严隔离可替换为容器化执行输出比对文本规范化 可选 SPJ多解判定、浮点误差判定结果回写批量 SQL 更新 状态机队列化、分布式判题扩展结果展示Web 前端读数据库排名、统计、图表扩展这七个环节缺任何一个都会在真实比赛里暴露问题。比如很多教学项目不区分“编译错误”和“运行错误”用户代码一崩就统一显示“答案错误”学生根本不知道是语法问题还是算法问题。所以环节拆分本质上是为状态机服务的后面第 4 章会详细讲。2.2 选型理由Java 管调度和进程MySQL 管状态和数据为什么标题里是 Java MySQL而不是 Python SQLite 或者其他组合我的判断是Java 在进程管理和工程化生态上有现成优势。判题必须把用户代码放到独立进程里跑Java 的 ProcessBuilder、ProcessHandle、destroyForcibly 这套 API 能在一个进程内完成“启动子进程、限时等待、收集输出、强制清理”的全流程不用引入额外的进程管理工具。至于并发Java 的多线程模型配合线程池处理几百个同时提交的压力是足够稳的真正要担心的是数据库连接池而不是线程池。MySQL 在这里承担的是“权威状态源”。提交记录、判题结果、题目的测试用例都要求强一致MySQL 的事务和行锁比内存缓存更可靠。有人问为什么不直接上 Redis 做队列和存储我一般会回答单机教学环境里 MySQL 足够等到需要集群判题时再加 Redis 或 Pika 做队列把 MySQL 留作最终落库。这也是“可扩展”的体现——不是一开始就上分布式而是留下替换边界。工程化上我建议用 Spring Boot 搭 Web 层、MyBatis 做持久层。这不是噱头而是因为 MyBatis 可以把复杂的判题结果批量更新 SQL 写得清晰写好的 XML 映射也是团队协作时最容易对齐的部分。JDBC 手写当然也行但遇到批量 insert、批量 update、JSON 字段读写时手写容易漏掉事务边界。这里有一个从标题里延伸出来的判断可扩展的起点不在框架在于每个环节是否独立、每个表是否留了扩展字段。3. 用 MySQL 建出可扩展的评测数据模型五张核心表与关键字段3.1 用户、题目、测试用例三张基础表字段设计与 JSON 扩展位下表的 schema 我做过多轮调整核心原则是基础字段稳定扩展字段用 JSON 留口子。先看用户表CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(64) NOT NULL COMMENT 登录名唯一, password_hash VARCHAR(128) NOT NULL COMMENT BCrypt 哈希不要存明文, role TINYINT NOT NULL DEFAULT 0 COMMENT 0-普通用户 1-管理员, school_no VARCHAR(32) DEFAULT COMMENT 学号/工号可按场景扩展, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-正常 0-禁用, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;password_hash 用 BCrypt不要用 MD5这个没有太多讨论余地。role 用 TINYINT 而不是字符串是为了索引效率和后续加角色时不用改表结构加权限系统时再拆一张角色表即可。接下来是题目表。题目表是扩展性设计的关键因为题型变化就发生在这里CREATE TABLE problem ( id BIGINT NOT NULL AUTO_INCREMENT, title VARCHAR(128) NOT NULL, description MEDIUMTEXT NOT NULL COMMENT 题目描述按需允许 HTML, input_spec TEXT COMMENT 输入说明, output_spec TEXT COMMENT 输出说明, difficulty TINYINT NOT NULL DEFAULT 1 COMMENT 1-简单 2-中等 3-困难, time_limit_ms INT NOT NULL DEFAULT 1000 COMMENT 单测试用例时间限制毫秒, memory_limit_kb INT NOT NULL DEFAULT 262144 COMMENT 内存限制单位KB默认256MB, judge_type VARCHAR(32) NOT NULL DEFAULT exact COMMENT exact-精确比 ou-浮点 spj-特判, extension_config JSON DEFAULT NULL COMMENT 扩展配置SPJ路径、测试点分数等, is_visible TINYINT NOT NULL DEFAULT 1, created_by BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_visible_diff (is_visible, difficulty) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT题目表;参数说明time_limit_ms 放在题目表是“默认值”每个测试用例还可以覆盖它memory_limit_kb 用 KB 而不是 MB是为了兼容一些需要精细控制内存的题目默认 262144 KB 恰好是 256 MB。judge_type 字段是“可扩展”的第一个开关——当题目需要多解判定时改成spj并配置 extension_config 里的 spj 程序路径。逻辑说明把判题类型放在题目层而不是测试用例层是因为一个题目的所有测试用例共享一种判定策略放在用例层会造成配置分散后面做数据迁移时很难对齐。测试用例表要特别注意题目可以有很多组测试数据而且测试数据一旦变大就不适合全部塞进数据库CREATE TABLE test_case ( id BIGINT NOT NULL AUTO_INCREMENT, problem_id BIGINT NOT NULL, test_group INT NOT NULL DEFAULT 1 COMMENT 分组一个测试点一组或按子任务分组, input_file_path VARCHAR(255) NOT NULL COMMENT 输入文件绝对路径或相对存储根路径, output_file_path VARCHAR(255) NOT NULL COMMENT 期望输出文件路径, time_limit_ms INT DEFAULT NULL COMMENT 为空时继承题目的 time_limit_ms, memory_limit_kb INT DEFAULT NULL, score INT NOT NULL DEFAULT 0 COMMENT 该测试点分值用于部分得分, is_sample TINYINT NOT NULL DEFAULT 0 COMMENT 是否样例用于前端展示, sort_order INT NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY idx_problem_group (problem_id, test_group, sort_order) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT测试用例表;逻辑说明input_file_path 和 output_file_path 存的是文件路径而非 TEXT 内容这是很多从 Excel 式设计转过来的人最容易翻车的地方。判题时 IO 读写文件比读数据库字段快一个数量级而且 MySQL 单包大小有限往库里塞几 MB 的输入数据会影响整库性能。score 字段支持部分得分比如 5 组测试点每组 20 分学生只跑过 3 组就是 60 分这对教学赛非常重要。3.2 提交表和判题结果表状态机与幂等设计提交表是整个系统最热的一张表设计时要同时照顾写入频率和查询场景CREATE TABLE submission ( id BIGINT NOT NULL AUTO_INCREMENT, submission_token VARCHAR(64) NOT NULL COMMENT 幂等键防止重复提交, user_id BIGINT NOT NULL, problem_id BIGINT NOT NULL, language VARCHAR(32) NOT NULL COMMENT java/cpp/python2/python3/go, source_code MEDIUMTEXT NOT NULL COMMENT 代码正文或存文件路径, code_file_path VARCHAR(255) DEFAULT NULL COMMENT 代码落盘路径供编译阶段读取, status VARCHAR(20) NOT NULL DEFAULT PENDING COMMENT 状态机见 4.1, score INT NOT NULL DEFAULT 0, used_time_ms INT DEFAULT NULL COMMENT 所有测试点最大耗时, used_memory_kb INT DEFAULT NULL COMMENT 所有测试点最大内存, error_message TEXT COMMENT 编译错误或运行时错误摘要, judge_type VARCHAR(32) NOT NULL DEFAULT exact, judged_at DATETIME DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_token (submission_token), KEY idx_user_problem (user_id, problem_id), KEY idx_status_created (status, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT提交记录表;参数说明submission_token 是我强烈建议加的字段。前端提交或者判题回写时如果网络抖动导致客户端重试没有幂等键就会出现同一份代码被判两次、成绩被覆盖的问题。idx_status_created索引用于判题 worker 扫描待判队列这条查询非常高频没有索引会造成全表扫描。status 用 VARCHAR 是为了在日志里直接可读配合代码里的枚举映射比数字更直观。判题结果表存的是“一次提交对应每个测试用例的明细”它让“AC/WA/TLE/MLE”这种判定变得可解释CREATE TABLE judge_result_detail ( id BIGINT NOT NULL AUTO_INCREMENT, submission_id BIGINT NOT NULL, test_case_id BIGINT NOT NULL, test_group INT NOT NULL, status VARCHAR(20) NOT NULL COMMENT 该测试点状态, used_time_ms INT DEFAULT NULL, used_memory_kb INT DEFAULT NULL, output_tail TEXT COMMENT 实际输出尾部便于前端展示差异, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_submission (submission_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT判题明细表;有这张表前端展示“哪个测试点 WA、哪个测试点 TLE”就很容易。output_tail 只存尾部而不是全部输出是因为完整输出可能几 MB存数据库会撑爆表只存最后 2KB 足够定位问题。这套五表模型用户、题目、测试用例、提交、判题明细基本覆盖了教学 OJ 的全部数据需求也为后面接排行榜、统计报表留好了 join 的入口。4. 用 Java 实现判题服务编译、沙箱运行、状态机与结果回写4.1 判题状态机从 PENDING 到 ACCEPTED 的九个状态判题服务是 OJ 的心脏状态机则是心脏的节拍。我常用的状态集合如下中间态PENDING排队中、JUDGING编译/运行中。终态ACCEPTED通过、WRONG_ANSWER答案错误、COMPILE_ERROR编译错误、RUNTIME_ERROR运行崩溃含段错误、非零退出、TIME_LIMIT_EXCEEDED超时、MEMORY_LIMIT_EXCEEDED内存超限、SYSTEM_ERROR判题器自身出错。为什么单独留 SYSTEM_ERROR因为判题服务自身也可能崩——比如编译进程被系统杀、临时目录满了这时不能怪用户代码。状态机流转顺序是PENDING - JUDGING - 每个测试点依次判定 - 汇总终态。需要注意的是多个测试点里只要有一个 TLE这次提交的最终状态就是 TLE不需要继续跑后面的测试点能省大量 CPU。但要注意部分得分场景如果题目配了 groups 按子任务计分则需要全部跑完并按分组统计。这个差异我在 5.2 节展开讲。4.2 用 ProcessBuilder 跑用户代码一个最小可用的沙箱Java 里最朴素的沙箱就是独立进程。看这段核心判题代码它能编译并运行一份用户提交的 C 代码public class JudgeRunner { public JudgeResult runSingleTest(String execPath, String inputPath, long timeLimitMs, long maxOutputBytes) throws IOException, InterruptedException { ProcessBuilder pb new ProcessBuilder(execPath); pb.redirectInput(new File(inputPath)); // 测试输入从文件读 pb.redirectErrorStream(false); // 分离 stdout 和 stderr ByteArrayOutputStream stdoutBuf new ByteArrayOutputStream(); ByteArrayOutputStream stderrBuf new ByteArrayOutputStream(); Process process pb.start(); Thread outThread new Thread(() - copyWithLimit(process.getInputStream(), stdoutBuf, maxOutputBytes)); Thread errThread new Thread(() - copyWithLimit(process.getErrorStream(), stderrBuf, maxOutputBytes)); outThread.start(); errThread.start(); long start System.nanoTime(); boolean finished process.waitFor(timeLimitMs, TimeUnit.MILLISECONDS); long elapsedMs (System.nanoTime() - start) / 1_000_000; if (!finished) { // 超时杀掉子进程及其后代 process.descendants().forEach(ph - ph.destroyForcibly()); process.destroyForcibly(); return JudgeResult.timeLimitExceeded(elapsedMs, stdoutBuf.toString()); } outThread.join(2000); errThread.join(2000); int exitCode process.exitValue(); if (exitCode ! 0) { return JudgeResult.runtimeError(exitCode, stderrBuf.toString(), elapsedMs); } return JudgeResult.ok(stdoutBuf.toString(), elapsedMs); } }逻辑说明redirectInput把测试用例的输入文件直接接到子进程的标准输入避免用 OutputStream 往子进程里写大文本造成阻塞。redirectErrorStream(false)是为了分别取 stdout 和 stderr——用户代码往 stderr 打印调试信息不能污染答案输出但出现运行错误时这些信息又是排查线索。参数说明waitFor 的第一个参数timeLimitMs直接取题目表配置一般 1000ms 到 3000ms 之间maxOutputBytes建议 1MB 到 2MB防止用户代码死循环刷屏把磁盘写满。内存限制在这个最小实现里没有做因为 Java 层读子进程内存得轮询/proc/pid/status代码会变得很长。我一般单独写一个 MemoryWatcher 线程循环读取/proc/{pid}/status里的 VmRSS超过memoryLimitKb就主动destroyForcibly()。这里有一个重要提示ProcessBuilder 只适合教研环境或低风险内网真正的生产 OJ 必须加容器隔离、seccomp 限制系统调用否则用户代码可以读到服务器上其他文件。标题里的“可扩展”指的是业务扩展性不是安全边界安全隔离必须另做一层。4.3 判题结果回写批量更新与幂等消费每个测试用例判完后不要立刻 update 数据库。正确做法是把本轮所有测试点结果先存在内存列表全部跑完后一次性更新submission和judge_result_detail。一个完整判题流程的 Java 伪代码如下Transactional public void judge(Submission submission, ListTestCase testCases) { int totalScore 0; String finalStatus ACCEPTED; ListJudgeResultDetail details new ArrayList(); for (TestCase tc : testCases) { JudgeResult r runner.runSingleTest(execPath, tc.getInputPath(), tc.getTimeLimitMs(), MAX_OUTPUT_BYTES); r.setStatus(normalizeStatus(r, tc)); // 将输出比对结果转为判定状态 details.add(toDetail(tc, r)); if (!r.isAccepted()) { finalStatus r.getStatus(); if (!supportsPartialScore(submission.getJudgeType())) break; } totalScore r.isAccepted() ? tc.getScore() : 0; } submissionMapper.updateResult(submission.getId(), finalStatus, totalScore, details.stream().mapToInt(JudgeResultDetail::getUsedTimeMs).max().orElse(0)); detailMapper.batchInsert(details); }逻辑说明Transactional保证提交表更新和明细插入是同生共死的避免出现“明细表有新记录提交表还是 PENDING”的数据不一致。break逻辑前面讲过不是部分得分题型时遇到 WA/TLE 直接终止节省判题资源。参数说明finalStatus 的优先级是 TLE/MLE 最高、WA 次之、RUNTIME_ERROR 再次、AC 最低因为运行到一半超时说明代码逻辑可能没跑完不能给出正确性判断。批量更新也不建议一次更新几百条明细MySQL 单条 insert 语句长度有限制。比较稳的做法是按照 50 到 100 条一批执行 batch insert如果失败就回滚整个事务。注意判题 worker 必须支持“重复消费同一 submission_token 时直接跳过”的幂等判断否则网络回写失败重试时会把成绩覆盖成第二次的脏数据。5. 可扩展设计落地与判题避坑指南新题型、新语言怎么低成本接入5.1 用策略模式接新语言和新题型扩展点选在哪“可扩展”不只在表结构里留 JSON 字段更要在 Java 代码里留接口。我常用的做法是定义一个 JudgeHandler 接口每种语言实现一个 Beanpublic interface JudgeHandler { String language(); // 返回 cpp/java/python3... CompileResult compile(Submission sub) throws Exception; RunResult run(TestCase tc, String execPath) throws Exception; }Component public class CppJudgeHandler implements JudgeHandler { Override public String language() { return cpp; } Override public CompileResult compile(Submission sub) throws Exception { Path src Paths.get(sub.getCodeFilePath()); Path exe src.resolveSibling(a.out); ProcessBuilder pb new ProcessBuilder(g, src.toString(), -o, exe.toString(), -O2, -stdc17); Process p pb.start(); boolean ok p.waitFor(10, TimeUnit.SECONDS); String err new String(p.getErrorStream().readAllBytes()); return new CompileResult(ok, exe.toString(), err); } Override public RunResult run(TestCase tc, String execPath) { // 调用 4.2 节 JudgeRunner上面已经有实现 return judgeRunner.runSingleTest(execPath, tc.getInputFilePath(), tc.getTimeLimitMs(), MAX_OUTPUT_BYTES); } }逻辑说明Spring 启动时会把所有 JudgeHandler 实现类收集进一个MapString, JudgeHandlerkey 就是 language() 的返回值。新接入一种语言只需要新增一个实现类不需要改判题主流程。参数说明-O2 -stdc17是 C 判题常用编译参数编译超时设置为 10 秒比运行超时长很多因为编译是可控的本地行为不应轻易限制。Java 语言同理用javac编译入口类名要按规则从代码里解析出来这里有一个容易踩的坑我会在 5.2 讲。题型扩展的做法是在判题主流程里只认judgeType精确匹配、浮点误差、Special Judge 各实现一个JudgeComparator。新增“输出忽略大小写”这类题型时加一个 Comparator 实现就够了。我一般把特判程序也做成可执行文件判题时把它和用户输出、期望输出一起传进去由特判程序决定 0/1/分数这样自由度最高。5.2 判题踩坑记录三个高发症状与修复方法踩坑一编译进程跑完了却不退出整个判题线程卡死。现象submission 状态一直停在 PENDING 或 JUDGING服务器进程列表里能看到残留的a.out或者java进程。原因用户代码里开了线程池或守护线程没有退出ProcessBuilder 只杀了直接子进程子进程的子进程还活着。解决超时后不要只调用 destroyForcibly要递归处理进程树——先process.descendants().forEach(ph - ph.destroyForcibly())再杀主进程必要时在 Linux 下对整个进程组执行kill -9 -pid。这段逻辑建议封装成destroyProcessTree(Process p)判题结束的 finally 块里统一调用别等到卡死才处理。踩坑二本机编译运行都正常提交上去就是 WRONG_ANSWER。现象同一份代码本地跑样例全过OJ 上 WA。原因一般有三个输出比对太严格末尾换行或空格差异被误判Windows 下编写的代码把\r\n带进了输出浮点数输出精度不一致。解决比对前做规范化——去掉输出字符串末尾所有空白字符把\r\n统一替换成\n浮点比对用Double.parseDouble后按 1e-6 精度比较。这里有个血泪经验WA 不一定是逻辑错也可能是输出全角半角标点问题规范化函数要写进判题框架的基础工具类里所有题型共用。踩坑三判题高峰期数据库连接被占满MySQL 报 Too many connections。现象比赛开始时大量用户同时提交Tomcat 日志开始刷连接池超时。原因判题 worker 每判一个测试点就 update 一次数据库几百个提交同时跑就把连接池打满。解决所有测试点判完再批量写库连接池最大连接数调到 50 到 100同时把判题服务的数据库连接和 Web 服务连接池分开。另一个优化是把judge_result_detail的 insert 改成批量不要一条条写。5.3 部署阶段的排查项JDK 环境、临时目录、日志可读性部署时最常见的坑是 Tomcat 启动后判题编译报错但现象很迷惑。如果你在 Servlet 容器里调 g 或 javac一定先确认外部进程能读到环境变量。我遇到过JAVA_HOME没配导致 javac 找不到但容器日志里只显示“编译失败”错误信息被吞了。解决在启动脚本里显式 export JAVA_HOME 和 PATH并且把编译错误流完整写进 error_message 字段不要只存布尔值。另一个高频问题是/tmp目录空间被写满。用户代码和可执行文件如果都放系统 /tmp长时间运行会被系统清理任务删除或者被磁盘占满。解决判题服务使用独立的工作目录比如/data/oj/tmp/{submissionId}每个提交跑完立刻清理该目录。这一步还要处理“清理失败”的情况定期写一个定时任务扫超过 2 小时没动的目录直接删掉。日志方面我必须强调在日志里带上submissionId、judgeType、language否则线上排查时你根本不知道这条日志对应哪个提交。这不算技巧算教训。6. 进阶判题集群化之前先做一轮实验验证再走这三步把单机 OJ 扩展成集群判题我建议不要急着上 Kubernetes先把三件事做扎实。第一步是压测用 JMeter 或者 ab 压POST /submit接口每个线程模拟一个用户提交同一道题观察 PENDING 到终态的耗时分布重点看 95 分位而不是平均值。压测时要故意混入超时代码和死循环代码看沙箱能不能把进程清理干净这一步能暴露大部分进程残留问题。第二步是验证幂等构造一个提交手动触发两次判题确认结果一致且 judge_result_detail 不会翻倍。第三步是看 MySQL 慢查询日志把submission和judge_result_detail的更新频率和索引使用情况整理成清单。实验验证通过后再走集群化的三步把判题服务从 Web 进程里拆成独立 Worker 进程Web 端只负责写 submission 记录引入一个简单的消息队列单机用 Redis 或 Pika 就够把 submissionId 推给多个 Worker 消费Worker 判完后调用 Web 端的回写接口更新结果回写接口必须做幂等。这三步走完你的标题里的“可扩展”就从表结构扩展到了部署架构扩展。我自己做这套改造时最深的体会是先别追求并发数先把“杀进程”“写结果”两个动作做可靠集群只是把同一个可靠动作并行跑而已。希望帮到你。本文还有配套的精品资源点击获取
返回列表