源码链接: https://github.com/kayden-0516/CodeBench-Distributed
一句话摘要: 本项目是一个使用 Spring Cloud Alibaba + Vue3 开发的在线编程评测系统,旨在解决编程学习者缺乏实践平台的核心痛点,实现了从题库管理、竞赛组织到代码自动评测的全流程服务。
技术栈: Spring Cloud Alibaba | Vue3 | MySQL | Redis | Docker | Nacos | RabbitMQ
引言:为什么我要做这个项目?
-
遇到的痛点:在编程学习过程中,我发现现有的在线评测平台要么功能过于简单,要么架构陈旧难以满足高并发需求。特别是对于想要学习微服务架构的开发者来说,缺乏一个完整的、现代化的实战项目参考。
-
项目目标 : 因此,我决定开发一个在线OJ系统,它能够提供完整的编程题目练习、竞赛组织和自动评测功能,同时采用现代化的微服务架构,帮助编程学习者获得更好的练习体验,也为开发者提供一个完整的企业级项目参考。
1. 项目概述与背景
1.1 项目背景与市场需求
在线评测系统(Online Judge)起源于ACM国际大学生程序设计竞赛,现已发展成为编程教育、技术面试、技能评估的重要平台。随着数字化转型加速,企业对程序员的技术评估需求日益增长,在线OJ系统成为连接学习者、教育机构和企业的重要桥梁。
市场分析:
-
教育市场:高校计算机课程实践平台、编程培训机构教学工具
-
企业市场:技术面试筛选、内部技能评估、技术竞赛举办
-
个人市场:编程爱好者技能提升、求职准备、技术交流
用户画像:

1.2 项目目标与核心价值
业务目标:
-
构建稳定可靠的在线编程评测平台
-
支持大规模并发代码提交和评测
-
提供完整的编程学习路径和竞赛体系
-
实现企业级的技术评估解决方案
技术目标:
-
采用微服务架构,保证系统可扩展性
-
实现前后端分离,提升开发效率
-
集成现代化开发工具和流程
-
确保系统安全性和高性能
2. 项目开发全流程详解
2.1 立项与需求分析阶段
2.1.1 需求收集方法论
多维度需求收集:
-
用户访谈深度分析
// 用户需求分析模型
public class UserRequirement {
private String userType; // 用户类型
private String usageScenario; // 使用场景
private String corePainPoint; // 核心痛点
private String expectedSolution; // 期望解决方案
private Integer priority; // 需求优先级
}// 典型用户需求示例
List<UserRequirement> requirements = Arrays.asList(
new UserRequirement("在校学生", "课程作业", "代码运行环境配置复杂", "在线代码执行", 1),
new UserRequirement求职者", "面试准备", "缺乏真实编程环境", "模拟面试题库", 2),
new UserRequirement("企业HR", "人才筛选", "技术评估效率低", "自动化技术测评", 1)
); -
竞品功能矩阵分析
功能模块 LeetCode 牛客网 我们的优势 题目数量 2000+ 1500+ 渐进式更新,质量优先 编程语言 10+ 5+ Java深度优化,扩展性强 竞赛系统 周赛/双周赛 企业专场 定制化竞赛支持 学习路径 付费课程 社区驱动 个性化推荐算法 -
技术可行性分析
-

2.1.2 需求规格说明书
功能需求详细分解:
-
用户管理模块
UserManagement:
Authentication:
- 用户注册(邮箱/手机号)
- 密码强度校验
- 登录状态保持
- 第三方登录(预留)
Profile:
- 个人信息维护
- 学习数据统计
- 成就系统
- 消息中心
题目管理模块
public class QuestionSpecification {
// 题目属性
private Long questionId;
private String title;
private String description;
private String difficulty; // EASY, MEDIUM, HARD
private List<String> tags;
private QuestionContent content;
// 测试用例
private List<TestCase> testCases;
private JudgeConfig judgeConfig;
// 统计信息
private QuestionStatistics statistics;
}
public class JudgeConfig {
private Integer timeLimit; // 时间限制(ms)
private Integer memoryLimit; // 内存限制(MB)
private Boolean specialJudge; // 是否特殊判题
private String judgeScript; // 判题脚本
}
竞赛系统模块
-- 竞赛数据模型
CREATE TABLE `tb_exam` (
`exam_id` BIGINT PRIMARY KEY COMMENT '竞赛ID',
`title` VARCHAR(100) NOT NULL COMMENT '竞赛标题',
`description` TEXT COMMENT '竞赛描述',
`start_time` DATETIME NOT NULL COMMENT '开始时间',
`end_time` DATETIME NOT NULL COMMENT '结束时间',
`duration` INT COMMENT '持续时间(分钟)',
`type` TINYINT COMMENT '竞赛类型1-公开赛2-私有赛',
`status` TINYINT DEFAULT 1 COMMENT '状态1-启用0-禁用',
`max_participants` INT COMMENT '最大参与人数',
`password` VARCHAR(50) COMMENT '访问密码',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP
) COMMENT='竞赛表';
2.2 技术选型与架构设计
2.2.1 架构演进思考
从单体到微服务的演进路径:
// 单体架构的问题示例
@Service
public class MonolithicOJService {
// 用户管理、题目管理、竞赛管理、判题服务全部耦合在一起
public SubmitResult submitCode(CodeSubmitRequest request) {
// 1. 验证用户权限
User user = userService.validateToken(request.getToken());
// 2. 检查题目状态
Question question = questionService.getById(request.getQuestionId());
// 3. 保存提交记录
SubmitRecord record = submitService.saveSubmitRecord(user, question, request.getCode());
// 4. 执行代码判题(阻塞操作)
JudgeResult result = judgeService.executeCode(record);
// 5. 更新用户数据
userService.updateUserStats(user, result);
// 6. 发送通知
notificationService.sendResultNotification(user, result);
return convertToDTO(result);
}
}
微服务拆分优势分析:
-
技术异构性:不同服务可选择最适合的技术栈
-
独立部署:服务可独立发布,降低发布风险
-
故障隔离:单个服务故障不影响整体系统
-
团队自治:不同团队负责不同服务,提升开发效率
2.2.2 技术栈深度解析
后端技术决策矩阵:
| 技术领域 | 技术选型 | 决策理由 | 替代方案对比 |
|---|---|---|---|
| 微服务框架 | Spring Cloud Alibaba | 阿里云生态集成、中文文档丰富、国内社区活跃 | Spring Cloud Netflix(停止维护)、Dubbo(功能相对单一) |
| 服务注册发现 | Nacos | 配置管理+服务发现二合一、AP架构保证高可用 | Eureka(2.x闭源)、Consul(运维复杂) |
| 配置中心 | Nacos Config | 与注册中心统一、支持配置热更新 | Spring Cloud Config(需要配合Git)、Apollo(部署复杂) |
| 流量控制 | Sentinel | 可视化控制台、多种流量控制策略 | Hystrix(停止维护)、Resilience4j(功能相对简单) |
| 分布式事务 | Seata | AT模式无侵入、支持多种事务模式 | 2PC(性能较差)、TCC(实现复杂) |
前端技术选型依据:
// Vue3 Composition API vs Options API
// Composition API 优势分析
import { ref, reactive, computed, onMounted } from 'vue'
export default {
setup() {
// 逻辑关注点分离,相关功能组织在一起
const { user, loadUser } = useUser()
const { questions, loadQuestions } = useQuestions()
const { submissions, loadSubmissions } = useSubmissions()
onMounted(() => {
loadUser()
loadQuestions()
loadSubmissions()
})
return {
user,
questions,
submissions
}
}
}
// 对应的Options API(逻辑分散)
export default {
data() {
return {
user: null,
questions: [],
submissions: []
}
},
methods: {
loadUser() { /* ... */ },
loadQuestions() { /* ... */ },
loadSubmissions() { /* ... */ }
},
mounted() {
this.loadUser()
this.loadQuestions()
this.loadSubmissions()
}
}
3. 微服务架构深度设计
3.1 服务拆分策略
3.1.1 领域驱动设计(DDD)实践
领域模型划分:
// 核心领域模型定义
@Entity
@Table(name = "tb_question")
public class Question implements AggregateRoot {
@Id
private Long questionId;
private String title;
private String description;
private QuestionDifficulty difficulty;
@Embedded
private QuestionContent content;
@OneToMany(mappedBy = "question")
private List<TestCase> testCases;
@Embedded
private JudgeConfig judgeConfig;
// 领域方法
public boolean validateCode(String code) {
// 代码基础验证逻辑
return code != null && !code.trim().isEmpty() && code.length() <= 10000;
}
public JudgeResult judgeSubmission(CodeSubmission submission) {
// 判题领域逻辑
if (!validateCode(submission.getCode())) {
return JudgeResult.invalid("代码格式错误");
}
return doJudge(submission);
}
}
// 值对象
@Embeddable
public class QuestionContent {
private String problemStatement;
private String inputFormat;
private String outputFormat;
private List<String> constraints;
private List<Example> examples;
}
// 枚举类型
public enum QuestionDifficulty {
EASY("简单", 1),
MEDIUM("中等", 2),
HARD("困难", 3);
private final String description;
private final int level;
QuestionDifficulty(String description, int level) {
this.description = description;
this.level = level;
}
}
3.1.2 服务边界定义
服务契约设计:
// 服务接口定义 - 题目服务
@FeignClient(name = "oj-question", path = "/api/questions")
public interface QuestionServiceClient {
@GetMapping("/{questionId}")
ResponseEntity<QuestionDTO> getQuestionById(@PathVariable Long questionId);
@PostMapping("/search")
ResponseEntity<PageResult<QuestionDTO>> searchQuestions(@RequestBody QuestionQuery query);
@GetMapping("/{questionId}/testcases")
ResponseEntity<List<TestCaseDTO>> getTestCases(@PathVariable Long questionId);
}
// 服务接口定义 - 用户服务
@FeignClient(name = "oj-user", path = "/api/users")
public interface UserServiceClient {
@GetMapping("/{userId}")
ResponseEntity<UserDTO> getUserById(@PathVariable Long userId);
@PostMapping("/{userId}/submissions")
ResponseEntity<Void> addUserSubmission(@PathVariable Long userId, @RequestBody SubmissionDTO submission);
@GetMapping("/{userId}/statistics")
ResponseEntity<UserStatistics> getUserStatistics(@PathVariable Long userId);
}
3.2 服务治理与通信
3.2.1 服务发现与负载均衡
Nacos服务注册配置:
# application.yml
spring:
application:
name: oj-question-service
cloud:
nacos:
discovery:
server-addr: ${NACOS_HOST:localhost}:8848
namespace: ${NACOS_NAMESPACE:bitcoj}
group: ${NACOS_GROUP:DEFAULT_GROUP}
cluster-name: ${NACOS_CLUSTER:DEFAULT}
# 服务元数据
metadata:
version: 1.0.0
environment: ${spring.profiles.active}
region: ${REGION:china-east}
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
namespace: ${spring.cloud.nacos.discovery.namespace}
group: ${spring.cloud.nacos.discovery.group}
file-extension: yaml
# 配置自动刷新
refresh-enabled: true
# 服务健康检查配置
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
负载均衡策略配置:
@Configuration
public class LoadBalancerConfiguration {
@Bean
@LoadBalancerClient(name = "oj-judge-service", configuration = JudgeServiceConfiguration.class)
public ReactorLoadBalancer<ServiceInstance> judgeServiceLoadBalancer(
Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RoundRobinLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
name
);
}
}
// 自定义负载均衡策略
public class JudgeServiceConfiguration {
@Bean
public ServiceInstanceListSupplier judgeServiceInstanceListSupplier() {
return ServiceInstanceListSupplier.builder()
.withBlockingDiscoveryClient()
.withSameInstancePreference() // 优先选择相同实例
.withHealthChecks() // 健康检查
.build();
}
}
3.2.2 服务间通信模式
同步通信 - OpenFeign增强配置:
@Configuration
public class FeignConfiguration {
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
public RequestInterceptor authRequestInterceptor() {
return template -> {
// 自动传递认证信息
String token = SecurityContextHolder.getContext().getAuthentication().getCredentials().toString();
template.header("Authorization", "Bearer " + token);
};
}
@Bean
public ErrorDecoder feignErrorDecoder() {
return (methodKey, response) -> {
if (response.status() == 401) {
return new UnauthorizedException("认证失败");
} else if (response.status() == 403) {
return new ForbiddenException("权限不足");
} else if (response.status() >= 500) {
return new ServiceUnavailableException("服务暂时不可用");
}
return new FeignException(response.status(), response.reason());
};
}
}
// 重试机制配置
@Bean
public Retryer feignRetryer() {
return new Retryer.Default(100, 1000, 3); // 重试3次,间隔100ms开始,最大间隔1s
}
异步通信 - 消息队列深度设计
// 消息类型定义
public abstract class BaseMessage implements Serializable {
protected String messageId;
protected LocalDateTime timestamp;
protected String sourceService;
protected MessageType messageType;
protected BaseMessage(MessageType messageType) {
this.messageId = UUID.randomUUID().toString();
this.timestamp = LocalDateTime.now();
this.sourceService = getCurrentServiceName();
this.messageType = messageType;
}
}
// 判题消息
public class JudgeMessage extends BaseMessage {
private Long submissionId;
private Long questionId;
private String code;
private String language;
private JudgeConfig judgeConfig;
public JudgeMessage() {
super(MessageType.JUDGE_SUBMISSION);
}
// 消息验证
public boolean validate() {
return submissionId != null && questionId != null
&& StringUtils.hasText(code) && StringUtils.hasText(language);
}
}
// 消息生产者服务
@Service
@Slf4j
public class MessageProducerService {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private ObjectMapper objectMapper;
public <T extends BaseMessage> void sendMessage(String exchange, String routingKey, T message) {
try {
// 消息预处理
message.preSend();
// 发送消息
rabbitTemplate.convertAndSend(exchange, routingKey, message, m -> {
// 设置消息属性
m.getMessageProperties().setMessageId(message.getMessageId());
m.getMessageProperties().setTimestamp(new Date());
m.getMessageProperties().setContentType("application/json");
m.getMessageProperties().setContentEncoding("UTF-8");
// 设置消息TTL(1小时)
m.getMessageProperties().setExpiration("3600000");
return m;
});
log.info("消息发送成功: messageId={}, type={}", message.getMessageId(), message.getMessageType());
} catch (Exception e) {
log.error("消息发送失败: messageId={}, error={}", message.getMessageId(), e.getMessage(), e);
throw new MessageSendException("消息发送失败", e);
}
}
// 延迟消息发送
public <T extends BaseMessage> void sendDelayedMessage(String exchange, String routingKey, T message, long delayMillis) {
rabbitTemplate.convertAndSend(exchange, routingKey, message, m -> {
m.getMessageProperties().setDelay(Math.toIntExact(delayMillis));
return m;
});
}
}
4. 核心业务模块实现
4.1 身份认证与安全体系
4.1.1 JWT + Redis混合认证方案
Token服务深度实现:
@Service
@Slf4j
public class TokenService {
@Autowired
private RedisService redisService;
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration:720}")
private Long expiration; // 默认12小时
private static final String LOGIN_TOKEN_KEY = "login_tokens:";
private static final String TOKEN_REFRESH_KEY = "token_refresh:";
/**
* 创建令牌 - 支持多端登录
*/
public TokenPair createToken(LoginUser loginUser, String clientType) {
String accessToken = UUID.fastUUID().toString();
String refreshToken = UUID.fastUUID().toString();
// 设置客户端信息
loginUser.setToken(accessToken);
loginUser.setClientType(clientType);
loginUser.setLoginTime(System.currentTimeMillis());
// 存储登录信息
storeLoginUser(loginUser, accessToken);
// 生成JWT
String jwtToken = generateJwtToken(loginUser, accessToken);
// 存储刷新令牌
storeRefreshToken(loginUser.getUserId(), clientType, refreshToken);
return new TokenPair(jwtToken, refreshToken, expiration);
}
/**
* 存储登录用户信息
*/
private void storeLoginUser(LoginUser loginUser, String accessToken) {
String userKey = getTokenKey(accessToken);
// 存储用户信息
redisService.setCacheObject(userKey, loginUser, expiration, TimeUnit.MINUTES);
// 存储用户-令牌映射(支持多端登录管理)
String userTokenKey = getUserTokenKey(loginUser.getUserId(), loginUser.getClientType());
redisService.setCacheObject(userTokenKey, accessToken, expiration, TimeUnit.MINUTES);
}
/**
* 生成JWT令牌
*/
private String generateJwtToken(LoginUser loginUser, String accessToken) {
Map<String, Object> claims = new HashMap<>();
claims.put(SecurityConstants.USER_KEY, accessToken);
claims.put(SecurityConstants.DETAILS_USER_ID, loginUser.getUserId());
claims.put(SecurityConstants.DETAILS_USERNAME, loginUser.getUsername());
claims.put(SecurityConstants.DETAILS_CLIENT_TYPE, loginUser.getClientType());
claims.put(SecurityConstants.LOGIN_TIME, loginUser.getLoginTime());
return JwtUtils.createToken(claims, secret);
}
/**
* 刷新令牌
*/
public TokenPair refreshToken(String refreshToken, String clientType) {
// 验证刷新令牌
Long userId = validateRefreshToken(refreshToken, clientType);
if (userId == null) {
throw new AuthenticationException("刷新令牌无效");
}
// 获取用户信息
LoginUser loginUser = loadUserById(userId);
if (loginUser == null) {
throw new AuthenticationException("用户不存在");
}
// 创建新令牌
return createToken(loginUser, clientType);
}
/**
* 令牌验证
*/
public LoginUser verifyToken(String token) {
try {
// 解析JWT
Claims claims = JwtUtils.parseToken(token, secret);
if (claims == null) {
return null;
}
// 获取访问令牌
String accessToken = JwtUtils.getUserKey(claims);
if (StringUtils.isEmpty(accessToken)) {
return null;
}
// 从Redis获取用户信息
String userKey = getTokenKey(accessToken);
LoginUser loginUser = redisService.getCacheObject(userKey, LoginUser.class);
if (loginUser != null) {
// 刷新令牌有效期
refreshToken(loginUser);
}
return loginUser;
} catch (ExpiredJwtException e) {
log.warn("令牌已过期: {}", e.getMessage());
throw new TokenExpiredException("令牌已过期");
} catch (Exception e) {
log.error("令牌验证失败: {}", e.getMessage());
return null;
}
}
/**
* 强制下线
*/
public void forceLogout(Long userId, String clientType) {
String userTokenKey = getUserTokenKey(userId, clientType);
String accessToken = redisService.getCacheObject(userTokenKey, String.class);
if (accessToken != null) {
// 删除登录信息
redisService.deleteObject(getTokenKey(accessToken));
redisService.deleteObject(userTokenKey);
// 删除刷新令牌
deleteRefreshToken(userId, clientType);
}
}
}
4.1.2 密码安全策略
增强型密码安全服务:
@Service
public class PasswordService {
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
private final Map<String, Integer> passwordAttempts = new ConcurrentHashMap<>();
private static final int MAX_ATTEMPTS = 5;
private static final long LOCK_DURATION = 15 * 60 * 1000; // 15分钟
/**
* 密码强度验证
*/
public PasswordStrength validatePasswordStrength(String password) {
if (StringUtils.isEmpty(password)) {
return PasswordStrength.EMPTY;
}
int score = 0;
// 长度检查
if (password.length() >= 8) score++;
if (password.length() >= 12) score++;
// 复杂度检查
if (password.matches(".*[a-z].*")) score++; // 小写字母
if (password.matches(".*[A-Z].*")) score++; // 大写字母
if (password.matches(".*\\d.*")) score++; // 数字
if (password.matches(".*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?].*")) score++; // 特殊字符
// 常见密码检查
if (isCommonPassword(password)) {
score = Math.max(0, score - 2);
}
return PasswordStrength.fromScore(score);
}
/**
* 加密密码
*/
public String encryptPassword(String password) {
PasswordStrength strength = validatePasswordStrength(password);
if (strength == PasswordStrength.WEAK) {
throw new WeakPasswordException("密码强度不足,请使用更复杂的密码");
}
return passwordEncoder.encode(password);
}
/**
* 密码验证(带尝试次数限制)
*/
public boolean matchesPassword(String rawPassword, String encodedPassword, String identifier) {
// 检查是否被锁定
if (isAccountLocked(identifier)) {
throw new AccountLockedException("账户因多次密码错误已被临时锁定");
}
boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);
if (matches) {
// 验证成功,重置尝试次数
passwordAttempts.remove(identifier);
} else {
// 验证失败,记录尝试次数
int attempts = passwordAttempts.getOrDefault(identifier, 0) + 1;
passwordAttempts.put(identifier, attempts);
if (attempts >= MAX_ATTEMPTS) {
// 锁定账户
lockAccount(identifier);
throw new AccountLockedException("密码错误次数过多,账户已被锁定15分钟");
}
}
return matches;
}
private boolean isAccountLocked(String identifier) {
Integer attempts = passwordAttempts.get(identifier);
return attempts != null && attempts >= MAX_ATTEMPTS;
}
private void lockAccount(String identifier) {
// 可以在这里记录锁定日志或发送通知
log.warn("账户被锁定: {}", identifier);
// 15分钟后自动解锁
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
passwordAttempts.remove(identifier);
log.info("账户自动解锁: {}", identifier);
}
}, LOCK_DURATION);
}
private boolean isCommonPassword(String password) {
Set<String> commonPasswords = Set.of(
"123456", "password", "12345678", "qwerty", "abc123",
"1234567", "111111", "1234", "admin", "password1"
);
return commonPasswords.contains(password.toLowerCase());
}
public enum PasswordStrength {
EMPTY, WEAK, MEDIUM, STRONG, VERY_STRONG;
public static PasswordStrength fromScore(int score) {
if (score <= 2) return WEAK;
if (score <= 4) return MEDIUM;
if (score <= 6) return STRONG;
return VERY_STRONG;
}
}
}
4.2 数据持久化设计
4.2.1 实体关系模型深度设计
完整的数据库架构:
-- 用户表增强设计
CREATE TABLE `tb_user` (
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID-雪花算法',
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
`email` VARCHAR(100) COMMENT '邮箱',
`phone` VARCHAR(20) COMMENT '手机号',
`password` VARCHAR(100) NOT NULL COMMENT '加密密码',
`nick_name` VARCHAR(50) NOT NULL COMMENT '昵称',
`avatar` VARCHAR(200) COMMENT '头像URL',
`gender` TINYINT COMMENT '性别0-未知1-男2-女',
`birthday` DATE COMMENT '生日',
`introduction` TEXT COMMENT '个人简介',
`school` VARCHAR(100) COMMENT '学校',
`company` VARCHAR(100) COMMENT '公司',
`position` VARCHAR(50) COMMENT '职位',
`user_type` TINYINT DEFAULT 1 COMMENT '用户类型1-普通用户2-管理员',
`status` TINYINT DEFAULT 1 COMMENT '状态1-正常0-禁用',
`last_login_time` DATETIME COMMENT '最后登录时间',
`last_login_ip` VARCHAR(50) COMMENT '最后登录IP',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`user_id`),
UNIQUE KEY `uk_username` (`username`),
UNIQUE KEY `uk_email` (`email`),
UNIQUE KEY `uk_phone` (`phone`),
KEY `idx_create_time` (`create_time`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
-- 题目表增强设计
CREATE TABLE `tb_question` (
`question_id` BIGINT UNSIGNED NOT NULL COMMENT '题目ID',
`title` VARCHAR(200) NOT NULL COMMENT '题目标题',
`description` TEXT NOT NULL COMMENT '题目描述',
`difficulty` TINYINT NOT NULL COMMENT '难度1-简单2-中等3-困难',
`tags` JSON COMMENT '题目标签',
`time_limit` INT NOT NULL DEFAULT 1000 COMMENT '时间限制(ms)',
`memory_limit` INT NOT NULL DEFAULT 128 COMMENT '内存限制(MB)',
`stack_limit` INT DEFAULT 128 COMMENT '堆栈限制(MB)',
`input_description` TEXT COMMENT '输入描述',
`output_description` TEXT COMMENT '输出描述',
`sample_input` TEXT COMMENT '样例输入',
`sample_output` TEXT COMMENT '样例输出',
`hint` TEXT COMMENT '提示',
`source` VARCHAR(100) COMMENT '题目来源',
`author_id` BIGINT UNSIGNED COMMENT '作者ID',
`visible` TINYINT DEFAULT 1 COMMENT '是否可见1-是0-否',
`submit_count` INT DEFAULT 0 COMMENT '提交次数',
`accept_count` INT DEFAULT 0 COMMENT '通过次数',
`accept_rate` DECIMAL(5,2) DEFAULT 0.00 COMMENT '通过率',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`question_id`),
KEY `idx_difficulty` (`difficulty`),
KEY `idx_author` (`author_id`),
KEY `idx_visible` (`visible`),
KEY `idx_create_time` (`create_time`),
KEY `idx_accept_rate` (`accept_rate`),
FULLTEXT KEY `ft_title_desc` (`title`, `description`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='题目表';
-- 提交记录表分区设计
CREATE TABLE `tb_submission` (
`submission_id` BIGINT UNSIGNED NOT NULL COMMENT '提交ID',
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
`question_id` BIGINT UNSIGNED NOT NULL COMMENT '题目ID',
`exam_id` BIGINT UNSIGNED COMMENT '竞赛ID',
`code` TEXT NOT NULL COMMENT '提交代码',
`language` VARCHAR(20) NOT NULL COMMENT '编程语言',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态0-待判题1-判题中2-成功3-失败',
`execute_time` INT COMMENT '执行时间(ms)',
`execute_memory` INT COMMENT '执行内存(KB)',
`judge_result` JSON COMMENT '判题结果',
`error_message` TEXT COMMENT '错误信息',
`ip_address` VARCHAR(50) COMMENT '提交IP',
`user_agent` TEXT COMMENT '用户代理',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`submission_id`, `create_time`),
KEY `idx_user_question` (`user_id`, `question_id`),
KEY `idx_user_exam` (`user_id`, `exam_id`),
KEY `idx_question_status` (`question_id`, `status`),
KEY `idx_create_time` (`create_time`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
PARTITION BY RANGE (YEAR(create_time)) (
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p2025 VALUES LESS THAN (2026),
PARTITION p_future VALUES LESS THAN MAXVALUE
) COMMENT='提交记录表';
4.2.2 MyBatis Plus深度集成
数据访问层增强设计:
// 基础Mapper接口
public interface BaseMapper<T> extends com.baomidou.mybatisplus.core.mapper.BaseMapper<T> {
/**
* 批量插入(性能优化版本)
*/
Integer insertBatchSomeColumn(Collection<T> entityList);
/**
* 逻辑删除
*/
Integer deleteByIdWithFill(T entity);
/**
* 乐观锁更新
*/
Integer updateByIdWithVersion(T entity);
}
// 题目Mapper定制化操作
@Mapper
public interface QuestionMapper extends BaseMapper<Question> {
/**
* 复杂查询:根据条件分页查询题目
*/
List<QuestionVO> selectQuestionList(@Param("query") QuestionQueryDTO query);
/**
* 更新题目统计信息
*/
@Update("UPDATE tb_question SET submit_count = submit_count + 1, " +
"accept_count = accept_count + #{accepted}, " +
"accept_rate = ROUND(accept_count * 100.0 / submit_count, 2) " +
"WHERE question_id = #{questionId}")
int updateQuestionStats(@Param("questionId") Long questionId, @Param("accepted") boolean accepted);
/**
* 获取题目详情(包含关联信息)
*/
@Select("SELECT q.*, u.nick_name as author_name " +
"FROM tb_question q LEFT JOIN tb_user u ON q.author_id = u.user_id " +
"WHERE q.question_id = #{questionId} AND q.visible = 1")
@Results({
@Result(property = "questionId", column = "question_id"),
@Result(property = "testCases", column = "question_id",
many = @Many(select = "selectTestCasesByQuestionId")),
@Result(property = "tags", column = "tags",
typeHandler = JsonTypeHandler.class)
})
QuestionDetailVO selectQuestionDetail(Long questionId);
/**
* 获取题目标签统计
*/
@Select("SELECT tag, COUNT(*) as count FROM tb_question, " +
"JSON_TABLE(tags, '$[*]' COLUMNS(tag VARCHAR(50) PATH '$')) AS tags " +
"WHERE visible = 1 GROUP BY tag ORDER BY count DESC")
List<TagCountVO> selectTagStatistics();
}
// 自定义类型处理器
@MappedTypes({List.class})
@MappedJdbcTypes(JdbcType.VARCHAR)
public class JsonTypeHandler extends BaseTypeHandler<List<String>> {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
List<String> parameter, JdbcType jdbcType) throws SQLException {
try {
ps.setString(i, objectMapper.writeValueAsString(parameter));
} catch (JsonProcessingException e) {
throw new SQLException("JSON序列化失败", e);
}
}
@Override
public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {
return parseJson(rs.getString(columnName));
}
private List<String> parseJson(String json) {
if (StringUtils.isEmpty(json)) {
return new ArrayList<>();
}
try {
return objectMapper.readValue(json,
objectMapper.getTypeFactory().constructCollectionType(List.class, String.class));
} catch (Exception e) {
return new ArrayList<>();
}
}
}
4.3 代码沙箱与判题引擎
4.3.1 Docker沙箱安全设计
安全的代码执行环境:
@Service
@Slf4j
public class SecureCodeSandbox {
@Autowired
private DockerClient dockerClient;
@Value("${sandbox.timeout:10000}")
private long timeout;
@Value("${sandbox.memory-limit:256m}")
private String memoryLimit;
@Value("${sandbox.cpu-shares:512}")
private int cpuShares;
/**
* 安全执行用户代码
*/
public ExecuteResult executeCodeSecurely(ExecuteRequest request) {
// 1. 代码安全检查
CodeSecurityCheckResult securityCheck = checkCodeSecurity(request.getCode(), request.getLanguage());
if (!securityCheck.isSafe()) {
return ExecuteResult.securityError(securityCheck.getRiskDescription());
}
// 2. 创建临时目录
Path tempDir = createSecureTempDirectory();
try {
// 3. 准备执行环境
ExecutionEnvironment env = prepareExecutionEnvironment(request, tempDir);
// 4. 创建安全容器
String containerId = createSecureContainer(env);
// 5. 执行代码并监控
ExecuteResult result = executeInContainerWithMonitoring(containerId, env);
return result;
} catch (Exception e) {
log.error("代码执行失败: {}", e.getMessage(), e);
return ExecuteResult.systemError("系统执行错误: " + e.getMessage());
} finally {
// 6. 清理资源
cleanup(tempDir);
}
}
/**
* 代码安全检查
*/
private CodeSecurityCheckResult checkCodeSecurity(String code, String language) {
CodeSecurityChecker checker = SecurityCheckerFactory.getChecker(language);
return checker.check(code);
}
/**
* 创建安全容器
*/
private String createSecureContainer(ExecutionEnvironment env) {
// 容器配置
HostConfig hostConfig = HostConfig.newHostConfig()
.withMemory(Long.parseLong(memoryLimit.replace("m", "")) * 1024 * 1024L)
.withMemorySwap(0L) // 禁用swap
.withCpuShares(cpuShares)
.withNetworkMode("none") // 禁用网络
.withCapDrop("ALL") // 删除所有权限
.withSecurityOpts(List.of("no-new-privileges:true"))
.withBinds(Bind.parse(env.getTempDir().toString() + ":/app:ro"));
// 创建容器
return dockerClient.createContainerCmd(env.getImageName())
.withHostConfig(hostConfig)
.withCmd(env.getExecutionCommand())
.withTty(true)
.withAttachStdin(true)
.withAttachStdout(true)
.withAttachStderr(true)
.exec()
.getId();
}
/**
* 带监控的代码执行
*/
private ExecuteResult executeInContainerWithMonitoring(String containerId, ExecutionEnvironment env) {
// 启动容器
dockerClient.startContainerCmd(containerId).exec();
// 执行超时控制
CompletableFuture<ExecuteResult> future = CompletableFuture.supplyAsync(() -> {
try {
return doExecute(containerId, env);
} catch (Exception e) {
return ExecuteResult.systemError("执行异常: " + e.getMessage());
}
});
try {
return future.get(timeout, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
// 超时处理
future.cancel(true);
forceKillContainer(containerId);
return ExecuteResult.timeoutError("执行超时");
} catch (Exception e) {
return ExecuteResult.systemError("执行失败: " + e.getMessage());
}
}
/**
* 强制终止容器
*/
private void forceKillContainer(String containerId) {
try {
dockerClient.stopContainerCmd(containerId).withTimeout(2).exec();
dockerClient.removeContainerCmd(containerId).exec();
} catch (Exception e) {
log.warn("容器清理失败: {}", e.getMessage());
}
}
}
// 代码安全检查器
public interface CodeSecurityChecker {
CodeSecurityCheckResult check(String code);
}
// Java代码安全检查器
@Component
public class JavaCodeSecurityChecker implements CodeSecurityChecker {
private static final Set<String> DANGEROUS_IMPORTS = Set.of(
"java.lang.reflect", "java.lang.invoke", "java.lang.ProcessBuilder",
"java.lang.Runtime", "java.lang.System", "java.io.File",
"java.net", "java.nio", "java.sql"
);
private static final Set<String> DANGEROUS_KEYWORDS = Set.of(
"Runtime.getRuntime()", "ProcessBuilder", "System.exit",
"File.", "Socket", "URLConnection", "Class.forName",
"Method.invoke", "Field.set", "Unsafe"
);
@Override
public CodeSecurityCheckResult check(String code) {
List<SecurityRisk> risks = new ArrayList<>();
// 检查危险导入
checkDangerousImports(code, risks);
// 检查危险关键字
checkDangerousKeywords(code, risks);
// 检查代码长度
if (code.length() > 10000) {
risks.add(new SecurityRisk("CODE_TOO_LONG", "代码长度超过限制"));
}
// 检查递归深度
checkRecursionDepth(code, risks);
return new CodeSecurityCheckResult(risks.isEmpty(), risks);
}
private void checkDangerousImports(String code, List<SecurityRisk> risks) {
for (String dangerousImport : DANGEROUS_IMPORTS) {
if (code.contains("import " + dangerousImport)) {
risks.add(new SecurityRisk("DANGEROUS_IMPORT",
"禁止导入: " + dangerousImport));
}
}
}
private void checkDangerousKeywords(String code, List<SecurityRisk> risks) {
for (String keyword : DANGEROUS_KEYWORDS) {
if (code.contains(keyword)) {
risks.add(new SecurityRisk("DANGEROUS_KEYWORD",
"禁止使用: " + keyword));
}
}
}
private void checkRecursionDepth(String code, List<SecurityRisk> risks) {
// 简单的递归深度检查
long recursionCount = countOccurrences(code, "public static void main");
if (recursionCount > 1) {
risks.add(new SecurityRisk("MULTIPLE_MAIN", "检测到多个main方法"));
}
}
private long countOccurrences(String text, String pattern) {
return Pattern.compile(pattern).matcher(text).results().count();
}
}
4.3.2 判题流程优化
异步判题流程设计:
@Service
@Slf4j
public class AsyncJudgeService {
@Autowired
private JudgeTaskDispatcher taskDispatcher;
@Autowired
private SubmissionService submissionService;
@Autowired
private QuestionService questionService;
@Autowired
private UserService userService;
/**
* 提交代码判题
*/
@Async("judgeTaskExecutor")
public CompletableFuture<JudgeResult> submitForJudge(CodeSubmission submission) {
return CompletableFuture.supplyAsync(() -> {
try {
// 1. 更新提交状态为判题中
submissionService.updateSubmissionStatus(submission.getSubmissionId(),
SubmissionStatus.JUDGING);
// 2. 获取题目信息和测试用例
Question question = questionService.getQuestionById(submission.getQuestionId());
List<TestCase> testCases = questionService.getTestCases(submission.getQuestionId());
// 3. 执行判题
JudgeResult result = executeJudgment(submission, question, testCases);
// 4. 更新判题结果
submissionService.updateJudgeResult(submission.getSubmissionId(), result);
// 5. 更新用户和题目统计
updateStatistics(submission, result, question);
// 6. 发送结果通知
sendResultNotification(submission, result);
return result;
} catch (Exception e) {
log.error("判题过程异常: submissionId={}", submission.getSubmissionId(), e);
submissionService.updateSubmissionStatus(submission.getSubmissionId(),
SubmissionStatus.SYSTEM_ERROR);
throw new JudgeException("判题系统异常", e);
}
});
}
/**
* 执行判题流程
*/
private JudgeResult executeJudgment(CodeSubmission submission, Question question, List<TestCase> testCases) {
JudgeResult result = new JudgeResult();
result.setSubmissionId(submission.getSubmissionId());
result.setQuestionId(submission.getQuestionId());
List<TestCaseResult> caseResults = new ArrayList<>();
boolean allPassed = true;
for (int i = 0; i < testCases.size(); i++) {
TestCase testCase = testCases.get(i);
// 执行单个测试用例
TestCaseResult caseResult = executeTestCase(submission, question, testCase, i + 1);
caseResults.add(caseResult);
if (!caseResult.isPassed()) {
allPassed = false;
// 如果第一个测试用例就失败,可以提前结束
if (question.getJudgeConfig().isStopOnFirstFailure() && i == 0) {
break;
}
}
// 检查执行时间是否超限
if (caseResult.getExecuteTime() > question.getTimeLimit()) {
caseResult.setPassed(false);
caseResult.setErrorMessage("时间超限");
allPassed = false;
break;
}
}
result.setTestCaseResults(caseResults);
result.setAllPassed(allPassed);
result.setTotalCases(testCases.size());
result.setPassedCases((int) caseResults.stream().filter(TestCaseResult::isPassed).count());
return result;
}
/**
* 执行单个测试用例
*/
private TestCaseResult executeTestCase(CodeSubmission submission, Question question,
TestCase testCase, int caseIndex) {
ExecuteRequest request = new ExecuteRequest();
request.setCode(submission.getCode());
request.setLanguage(submission.getLanguage());
request.setInput(testCase.getInput());
request.setTimeLimit(question.getTimeLimit());
request.setMemoryLimit(question.getMemoryLimit());
ExecuteResult executeResult = codeSandbox.executeCode(request);
TestCaseResult caseResult = new TestCaseResult();
caseResult.setCaseIndex(caseIndex);
caseResult.setInput(testCase.getInput());
caseResult.setExpectedOutput(testCase.getExpectedOutput());
caseResult.setActualOutput(executeResult.getOutput());
caseResult.setExecuteTime(executeResult.getExecuteTime());
caseResult.setExecuteMemory(executeResult.getExecuteMemory());
caseResult.setErrorMessage(executeResult.getErrorMessage());
// 验证输出结果
boolean passed = validateOutput(executeResult.getOutput(), testCase.getExpectedOutput(),
question.getJudgeConfig());
caseResult.setPassed(passed);
return caseResult;
}
/**
* 验证输出结果
*/
private boolean validateOutput(String actual, String expected, JudgeConfig judgeConfig) {
if (actual == null || expected == null) {
return false;
}
// 标准化输出(去除首尾空白字符)
String normalizedActual = actual.trim().replaceAll("\\r\\n", "\n");
String normalizedExpected = expected.trim().replaceAll("\\r\\n", "\n");
if (judgeConfig.isSpecialJudge()) {
// 特殊判题逻辑
return specialJudge(normalizedActual, normalizedExpected, judgeConfig.getJudgeScript());
} else {
// 普通判题:精确匹配
return normalizedActual.equals(normalizedExpected);
}
}
}
4.4 消息系统设计
4.4.1 消息类型与路由设计
完整的消息体系:
// 消息类型枚举
public enum MessageType {
// 系统消息
SYSTEM_ANNOUNCEMENT("系统公告", "system", Priority.HIGH),
SYSTEM_MAINTENANCE("系统维护", "system", Priority.HIGH),
// 用户消息
USER_WELCOME("欢迎消息", "user", Priority.LOW),
USER_ACHIEVEMENT("成就解锁", "user", Priority.MEDIUM),
// 提交相关
SUBMISSION_RESULT("提交结果", "submission", Priority.HIGH),
SUBMISSION_REVIEW("代码评审", "submission", Priority.MEDIUM),
// 竞赛相关
EXAM_INVITATION("竞赛邀请", "exam", Priority.MEDIUM),
EXAM_REMINDER("竞赛提醒", "exam", Priority.MEDIUM),
EXAM_RESULT("竞赛结果", "exam", Priority.HIGH),
// 社交相关
FOLLOW_NOTIFICATION("关注通知", "social", Priority.LOW),
LIKE_NOTIFICATION("点赞通知", "social", Priority.LOW),
COMMENT_NOTIFICATION("评论通知", "social", Priority.MEDIUM);
private final String description;
private final String category;
private final Priority priority;
MessageType(String description, String category, Priority priority) {
this.description = description;
this.category = category;
this.priority = priority;
}
}
// 消息优先级
public enum Priority {
LOW(1), MEDIUM(2), HIGH(3), URGENT(4);
private final int level;
Priority(int level) {
this.level = level;
}
public int getLevel() {
return level;
}
}
// 基础消息类
@Data
@Builder
public class BaseMessage implements Serializable {
private String messageId;
private MessageType messageType;
private String title;
private String content;
private Map<String, Object> payload;
private Long senderId;
private List<Long> receiverIds;
private LocalDateTime sendTime;
private LocalDateTime expireTime;
private Priority priority;
private Map<String, String> attributes;
// 消息验证
public boolean validate() {
return StringUtils.hasText(messageId) &&
messageType != null &&
StringUtils.hasText(title) &&
sendTime != null &&
(receiverIds != null && !receiverIds.isEmpty());
}
// 消息预处理
public void preSend() {
if (this.messageId == null) {
this.messageId = UUID.randomUUID().toString();
}
if (this.sendTime == null) {
this.sendTime = LocalDateTime.now();
}
if (this.priority == null) {
this.priority = Priority.MEDIUM;
}
}
}
4.4.2 消息队列深度配置
RabbitMQ配置优化:
@Configuration
@Slf4j
public class RabbitMQAdvancedConfig {
@Bean
public Jackson2JsonMessageConverter messageConverter() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return new Jackson2JsonMessageConverter(objectMapper);
}
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
ConnectionFactory connectionFactory,
Jackson2JsonMessageConverter messageConverter) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(messageConverter);
factory.setConcurrentConsumers(3); // 并发消费者数量
factory.setMaxConcurrentConsumers(10); // 最大并发消费者
factory.setPrefetchCount(10); // 每次预取消息数量
factory.setDefaultRequeueRejected(false); // 拒绝的消息不重新入队
// 错误处理
factory.setErrorHandler(new ConditionalRejectingErrorHandler(
new FatalExceptionStrategy()));
// 确认模式
factory.setAcknowledgeMode(AcknowledgeMode.AUTO);
return factory;
}
// 判题消息队列配置
@Bean
public Queue judgeQueue() {
return QueueBuilder.durable(RabbitMQConstants.JUDGE_QUEUE)
.withArgument("x-dead-letter-exchange", RabbitMQConstants.DLX_EXCHANGE)
.withArgument("x-dead-letter-routing-key", RabbitMQConstants.JUDGE_QUEUE + ".dlq")
.withArgument("x-message-ttl", 3600000) // 1小时TTL
.withArgument("x-max-length", 10000) // 最大队列长度
.build();
}
// 死信队列配置
@Bean
public Queue judgeDlq() {
return QueueBuilder.durable(RabbitMQConstants.JUDGE_QUEUE + ".dlq")
.withArgument("x-message-ttl", 86400000) // 24小时TTL
.withArgument("x-max-length", 1000)
.build();
}
@Bean
public DirectExchange dlxExchange() {
return new DirectExchange(RabbitMQConstants.DLX_EXCHANGE);
}
@Bean
public Binding dlqBinding() {
return BindingBuilder.bind(judgeDlq())
.to(dlxExchange())
.with(RabbitMQConstants.JUDGE_QUEUE + ".dlq");
}
// 消息监听器
@Component
@Slf4j
public class JudgeMessageListener {
@Autowired
private JudgeService judgeService;
@RabbitListener(queues = RabbitMQConstants.JUDGE_QUEUE)
@RabbitHandler
public void handleJudgeMessage(JudgeMessage message,
Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
try {
log.info("开始处理判题消息: messageId={}, submissionId={}",
message.getMessageId(), message.getSubmissionId());
// 执行判题逻辑
judgeService.executeJudge(message);
// 手动确认消息
channel.basicAck(deliveryTag, false);
log.info("判题消息处理完成: messageId={}", message.getMessageId());
} catch (Exception e) {
log.error("判题消息处理失败: messageId={}", message.getMessageId(), e);
try {
// 判断是否应该重试
if (shouldRetry(message, e)) {
// 拒绝消息并重新入队
channel.basicNack(deliveryTag, false, true);
} else {
// 拒绝消息并不重新入队(进入死信队列)
channel.basicNack(deliveryTag, false, false);
}
} catch (IOException ioException) {
log.error("消息拒绝失败: messageId={}", message.getMessageId(), ioException);
}
}
}
private boolean shouldRetry(JudgeMessage message, Exception e) {
// 根据异常类型决定是否重试
if (e instanceof TemporaryFailureException) {
return true;
}
if (e instanceof JudgeTimeoutException) {
return false; // 超时错误不重试
}
// 默认重试3次
return message.getRetryCount() < 3;
}
}
}
// 消息重试机制
@Component
public class MessageRetryService {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 带重试的消息发送
*/
public <T extends BaseMessage> void sendWithRetry(String exchange, String routingKey,
T message, int maxRetries) {
int attempt = 0;
while (attempt <= maxRetries) {
try {
rabbitTemplate.convertAndSend(exchange, routingKey, message);
log.info("消息发送成功: messageId={}, attempt={}", message.getMessageId(), attempt);
return;
} catch (Exception e) {
attempt++;
log.warn("消息发送失败: messageId={}, attempt={}, error={}",
message.getMessageId(), attempt, e.getMessage());
if (attempt > maxRetries) {
log.error("消息发送最终失败: messageId={}, maxRetries={}",
message.getMessageId(), maxRetries);
throw new MessageSendException("消息发送失败,已达到最大重试次数", e);
}
// 指数退避
try {
long delay = (long) Math.pow(2, attempt) * 1000; // 2^attempt seconds
Thread.sleep(Math.min(delay, 30000)); // 最大延迟30秒
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new MessageSendException("消息发送被中断", ie);
}
}
}
}
5. 前端架构与工程化
5.1 组件化架构设计
企业级Vue3项目结构深度设计:
src/
├── apis/ # API接口层
│ ├── modules/ # 模块化API
│ │ ├── question.js # 题目相关API
│ │ ├── exam.js # 竞赛相关API
│ │ ├── user.js # 用户相关API
│ │ ├── submission.js # 提交记录API
│ │ └── message.js # 消息系统API
│ ├── interceptors/ # 请求拦截器
│ │ ├── request.js # 请求拦截
│ │ └── response.js # 响应拦截
│ ├── constants/ # API常量
│ │ ├── endpoints.js # 接口端点
│ │ └── error-codes.js # 错误码
│ └── index.js # API入口
├── assets/ # 静态资源
│ ├── styles/ # 全局样式
│ │ ├── variables.scss # SCSS变量
│ │ ├── mixins.scss # SCSS混入
│ │ ├── global.scss # 全局样式
│ │ ├── theme/ # 主题样式
│ │ │ ├── light.scss
│ │ │ └── dark.scss
│ │ └── components/ # 组件样式
│ └── images/ # 图片资源
│ ├── icons/ # 图标
│ ├── backgrounds/ # 背景图
│ └── avatars/ # 头像
├── components/ # 组件库
│ ├── common/ # 通用组件
│ │ ├── OJButton/ # 按钮组件
│ │ │ ├── index.vue
│ │ │ └── props.ts
│ │ ├── OJInput/ # 输入框组件
│ │ ├── OJTable/ # 表格组件
│ │ ├── OJCodeEditor/ # 代码编辑器
│ │ ├── OJLoading/ # 加载组件
│ │ └── OJModal/ # 模态框组件
│ ├── business/ # 业务组件
│ │ ├── QuestionCard/ # 题目卡片
│ │ ├── SubmissionList/ # 提交列表
│ │ ├── ExamTimer/ # 竞赛计时器
│ │ ├── CodeResult/ # 代码执行结果
│ │ └── UserRank/ # 用户排名
│ └── layout/ # 布局组件
│ ├── AppHeader/ # 顶部导航
│ ├── AppSidebar/ # 侧边栏
│ ├── AppFooter/ # 底部
│ └── PageContainer/# 页面容器
├── composables/ # Vue3组合式函数
│ ├── usePagination.js # 分页逻辑
│ ├── useCodeEditor.js # 代码编辑器逻辑
│ ├── useUser.js # 用户状态管理
│ ├── useWebSocket.js # WebSocket连接
│ ├── useTheme.js # 主题切换
│ ├── usePermission.js # 权限管理
│ └── useLocalStorage.js # 本地存储
├── router/ # 路由配置
│ ├── index.js # 路由入口
│ ├── routes/ # 路由模块
│ │ ├── question.js # 题目路由
│ │ ├── exam.js # 竞赛路由
│ │ ├── user.js # 用户路由
│ │ └── admin.js # 管理路由
│ └── guards/ # 路由守卫
│ ├── auth.js # 认证守卫
│ ├── permission.js # 权限守卫
│ └── progress.js # 进度条守卫
├── stores/ # 状态管理
│ ├── modules/ # Store模块
│ │ ├── user.js # 用户状态
│ │ ├── question.js # 题目状态
│ │ ├── exam.js # 竞赛状态
│ │ └── app.js # 应用状态
│ └── index.js # Store入口
├── utils/ # 工具函数
│ ├── auth.js # 认证工具
│ ├── request.js # 请求工具
│ ├── validator.js # 表单验证
│ ├── constants.js # 常量定义
│ ├── formatter.js # 格式化工具
│ ├── storage.js # 存储工具
│ └── helper.js # 辅助函数
├── views/ # 页面组件
│ ├── question/ # 题目相关页面
│ │ ├── List.vue # 题目列表
│ │ ├── Detail.vue # 题目详情
│ │ └── Solve.vue # 解题页面
│ ├── exam/ # 竞赛相关页面
│ │ ├── List.vue # 竞赛列表
│ │ ├── Detail.vue # 竞赛详情
│ │ ├── Playing.vue # 竞赛进行中
│ │ └── Result.vue # 竞赛结果
│ ├── user/ # 用户相关页面
│ │ ├── Profile.vue # 个人资料
│ │ ├── Submissions.vue # 提交记录
│ │ └── Messages.vue # 我的消息
│ └── admin/ # 管理后台
│ ├── Dashboard.vue # 仪表板
│ ├── QuestionManagement.vue # 题目管理
│ └── UserManagement.vue # 用户管理
└── main.js # 应用入口
API层深度设计:
// apis/modules/question.js
import request from '@/utils/request'
// 题目相关API
export const questionApi = {
// 获取题目列表
getQuestionList(params) {
return request({
url: '/question/list',
method: 'get',
params
})
},
// 获取题目详情
getQuestionDetail(questionId) {
return request({
url: `/question/detail/${questionId}`,
method: 'get'
})
},
// 搜索题目
searchQuestions(keyword, filters = {}) {
return request({
url: '/question/search',
method: 'post',
data: {
keyword,
...filters
}
})
},
// 创建题目(管理员)
createQuestion(questionData) {
return request({
url: '/question/create',
method: 'post',
data: questionData
})
},
// 更新题目(管理员)
updateQuestion(questionId, questionData) {
return request({
url: `/question/update/${questionId}`,
method: 'put',
data: questionData
})
},
// 获取题目统计
getQuestionStats(questionId) {
return request({
url: `/question/stats/${questionId}`,
method: 'get'
})
}
}
// apis/interceptors/request.js
import { getToken } from '@/utils/auth'
import { ElMessage } from 'element-plus'
let pendingRequests = new Map()
// 生成请求key
function generateReqKey(config) {
const { method, url, params, data } = config
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}
// 添加请求到pending
function addPendingRequest(config) {
const requestKey = generateReqKey(config)
config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
if (!pendingRequests.has(requestKey)) {
pendingRequests.set(requestKey, cancel)
}
})
}
// 移除pending请求
function removePendingRequest(config) {
const requestKey = generateReqKey(config)
if (pendingRequests.has(requestKey)) {
const cancel = pendingRequests.get(requestKey)
cancel(requestKey)
pendingRequests.delete(requestKey)
}
}
export const requestInterceptor = {
onFulfilled: (config) => {
// 移除重复请求
removePendingRequest(config)
// 添加当前请求
addPendingRequest(config)
// 添加认证token
const token = getToken()
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
// 设置内容类型
if (!config.headers['Content-Type']) {
config.headers['Content-Type'] = 'application/json'
}
// 添加时间戳防止缓存
if (config.method === 'get') {
config.params = {
...config.params,
_t: Date.now()
}
}
// 显示加载提示
if (config.showLoading !== false) {
config.loadingInstance = ElLoading.service({
lock: true,
text: '加载中...',
background: 'rgba(0, 0, 0, 0.7)'
})
}
return config
},
onRejected: (error) => {
return Promise.reject(error)
}
}
// apis/interceptors/response.js
import { ElMessage, ElMessageBox } from 'element-plus'
import { removeToken } from '@/utils/auth'
import router from '@/router'
export const responseInterceptor = {
onFulfilled: (response) => {
// 移除pending请求
const config = response.config
const requestKey = generateReqKey(config)
pendingRequests.delete(requestKey)
// 关闭加载提示
if (config.loadingInstance) {
config.loadingInstance.close()
}
const { data } = response
const { code, message } = data
// 业务成功
if (code === 1000) {
return data
}
// 业务错误处理
switch (code) {
case 3001: // 未授权
ElMessage.warning('登录已过期,请重新登录')
removeToken()
router.push('/login')
break
case 3002: // 参数错误
ElMessage.warning(message || '参数错误')
break
case 3103: // 登录失败
ElMessage.error(message || '用户名或密码错误')
break
default:
ElMessage.error(message || '操作失败')
}
return Promise.reject(new Error(message || 'Error'))
},
onRejected: (error) => {
// 关闭加载提示
if (error.config && error.config.loadingInstance) {
error.config.loadingInstance.close()
}
// 移除pending请求
if (error.config) {
const requestKey = generateReqKey(error.config)
pendingRequests.delete(requestKey)
}
// 错误处理
if (axios.isCancel(error)) {
console.log('请求被取消:', error.message)
return Promise.reject(new Error('请求被取消'))
}
if (!error.response) {
ElMessage.error('网络错误,请检查网络连接')
return Promise.reject(error)
}
const { status, data } = error.response
switch (status) {
case 401:
ElMessage.warning('未授权,请重新登录')
removeToken()
router.push('/login')
break
case 403:
ElMessage.warning('没有权限访问该资源')
break
case 404:
ElMessage.warning('请求的资源不存在')
break
case 500:
ElMessage.error('服务器内部错误')
break
case 502:
ElMessage.error('网关错误')
break
case 503:
ElMessage.error('服务暂时不可用')
break
case 504:
ElMessage.error('网关超时')
break
default:
ElMessage.error(data?.message || `请求错误: ${status}`)
}
return Promise.reject(error)
}
}
5.2 高级代码编辑器实现
Monaco Editor深度集成与优化:
### 5.2 高级代码编辑器实现
**Monaco Editor深度集成**:
```vue
<template>
<div class="code-editor-container">
<div class="editor-header">
<div class="language-selector">
<el-select v-model="currentLanguage" @change="handleLanguageChange">
<el-option
v-for="lang in supportedLanguages"
:key="lang.value"
:label="lang.label"
:value="lang.value"
/>
</el-select>
</div>
<div class="editor-actions">
<el-button @click="handleFormat" :disabled="!supportsFormatting">
<i class="icon-format"></i>格式化
</el-button>
<el-button @click="handleRun" type="primary">
<i class="icon-run"></i>运行代码
</el-button>
<el-button @click="handleSubmit" type="success">
<i class="icon-submit"></i>提交代码
</el-button>
</div>
</div>
<div class="editor-wrapper">
<monaco-editor
ref="editorRef"
v-model="code"
:language="currentLanguage"
:theme="editorTheme"
:options="editorOptions"
@change="onCodeChange"
@mount="onEditorMount"
/>
</div>
<div class="editor-footer">
<div class="status-bar">
<span>行: {{ cursorPosition.lineNumber }}, 列: {{ cursorPosition.column }}</span>
<span>编码: UTF-8</span>
<span>语言: {{ currentLanguage.toUpperCase() }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useStore } from 'vuex'
import { ElMessage } from 'element-plus'
import { MonacoEditor } from '@/components/common'
const props = defineProps({
questionId: {
type: String,
required: true
},
initialCode: {
type: String,
default: ''
},
readOnly: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['code-change', 'code-run', 'code-submit'])
const store = useStore()
const editorRef = ref(null)
const monacoInstance = ref(null)
// 响应式数据
const code = ref(props.initialCode || '')
const currentLanguage = ref('java')
const cursorPosition = ref({ lineNumber: 1, column: 1 })
const isEditorReady = ref(false)
// 支持的语言列表
const supportedLanguages = computed(() => [
{ value: 'java', label: 'Java', defaultCode: getJavaTemplate() },
{ value: 'python', label: 'Python', defaultCode: getPythonTemplate() },
{ value: 'cpp', label: 'C++', defaultCode: getCppTemplate() },
{ value: 'javascript', label: 'JavaScript', defaultCode: getJavascriptTemplate() }
])
// 编辑器配置
const editorOptions = computed(() => ({
fontSize: 14,
lineNumbers: 'on',
roundedSelection: false,
scrollBeyondLastLine: false,
readOnly: props.readOnly,
automaticLayout: true,
minimap: { enabled: true },
wordWrap: 'on',
lineHeight: 20,
letterSpacing: 0.5,
cursorBlinking: 'blink',
tabSize: 4,
insertSpaces: true,
detectIndentation: true,
folding: true,
foldingHighlight: true,
showFoldingControls: 'mouseover',
matchBrackets: 'always',
scrollbar: {
vertical: 'visible',
horizontal: 'visible',
useShadows: false
},
// 代码补全
quickSuggestions: true,
parameterHints: { enabled: true },
// 错误检查
glyphMargin: true,
lightbulb: { enabled: true },
// 主题相关
colorDecorators: true
}))
const editorTheme = ref('vs-dark')
// 是否支持格式化
const supportsFormatting = computed(() =>
['javascript', 'typescript', 'json', 'html', 'css'].includes(currentLanguage.value)
)
// 编辑器事件处理
const onEditorMount = (editor, monaco) => {
monacoInstance.value = monaco
isEditorReady.value = true
// 注册自定义主题
monaco.editor.defineTheme('oj-theme', {
base: 'vs-dark',
inherit: true,
rules: [
{ token: 'comment', foreground: '6A9955', fontStyle: 'italic' },
{ token: 'keyword', foreground: 'C586C0' },
{ token: 'string', foreground: 'CE9178' },
{ token: 'number', foreground: 'B5CEA8' }
],
colors: {
'editor.background': '#1E1E1E',
'editor.foreground': '#D4D4D4'
}
})
monaco.editor.setTheme('oj-theme')
// 监听光标位置变化
editor.onDidChangeCursorPosition((e) => {
cursorPosition.value = e.position
})
// 注册代码补全
registerCompletionProvider(monaco)
}
const onCodeChange = (value) => {
emit('code-change', {
code: value,
language: currentLanguage.value
})
// 自动保存到本地存储
saveToLocalStorage()
}
// 语言切换处理
const handleLanguageChange = (newLanguage) => {
const languageConfig = supportedLanguages.value.find(lang => lang.value === newLanguage)
if (languageConfig && code.value === getDefaultCode(currentLanguage.value)) {
code.value = languageConfig.defaultCode
}
currentLanguage.value = newLanguage
}
// 代码格式化
const handleFormat = async () => {
if (!monacoInstance.value || !editorRef.value) return
try {
const editor = editorRef.value.getEditor()
const action = editor.getAction('editor.action.formatDocument')
if (action) {
await action.run()
ElMessage.success('代码格式化完成')
}
} catch (error) {
ElMessage.error('格式化失败: ' + error.message)
}
}
// 运行代码
const handleRun = () => {
if (!validateCode()) return
emit('code-run', {
code: code.value,
language: currentLanguage.value,
questionId: props.questionId
})
}
// 提交代码
const handleSubmit = () => {
if (!validateCode()) return
emit('code-submit', {
code: code.value,
language: currentLanguage.value,
questionId: props.questionId
})
}
// 代码验证
const validateCode = () => {
if (!code.value.trim()) {
ElMessage.warning('代码不能为空')
return false
}
if (code.value.length > 10000) {
ElMessage.warning('代码长度不能超过10000个字符')
return false
}
return true
}
// 代码模板
function getJavaTemplate() {
return `import java.util.*;
import java.io.*;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 你的代码在这里
scanner.close();
}
}`
}
function getPythonTemplate() {
return `import sys
def main():
# 你的代码在这里
pass
if __name__ == "__main__":
main()`
}
// 本地存储管理
function saveToLocalStorage() {
const key = `code_${props.questionId}_${currentLanguage.value}`
localStorage.setItem(key, code.value)
}
function loadFromLocalStorage() {
const key = `code_${props.questionId}_${currentLanguage.value}`
const savedCode = localStorage.getItem(key)
if (savedCode) {
code.value = savedCode
}
}
// 代码补全提供者
function registerCompletionProvider(monaco) {
monaco.languages.registerCompletionItemProvider(currentLanguage.value, {
provideCompletionItems: (model, position) => {
const word = model.getWordUntilPosition(position)
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn
}
const suggestions = []
// Java特定的代码补全
if (currentLanguage.value === 'java') {
suggestions.push(
{
label: 'System.out.println',
kind: monaco.languages.CompletionItemKind.Function,
documentation: '输出文本到控制台',
insertText: 'System.out.println(${1:""});',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
range: range
},
{
label: 'public static void main',
kind: monaco.languages.CompletionItemKind.Snippet,
documentation: '主方法模板',
insertText: [
'public static void main(String[] args) {',
'\t${1:// code here}',
'}'
].join('\n'),
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
range: range
}
)
}
return { suggestions }
}
})
}
// 生命周期
onMounted(() => {
loadFromLocalStorage()
})
onUnmounted(() => {
// 清理资源
if (monacoInstance.value && editorRef.value) {
const editor = editorRef.value.getEditor()
editor.dispose()
}
})
// 监听props变化
watch(() => props.initialCode, (newCode) => {
if (newCode && newCode !== code.value) {
code.value = newCode
}
})
// 暴露方法给父组件
defineExpose({
getCode: () => code.value,
setCode: (newCode) => { code.value = newCode },
getLanguage: () => currentLanguage.value,
setLanguage: (lang) => { currentLanguage.value = lang },
formatCode: handleFormat
})
</script>
<style lang="scss" scoped>
.code-editor-container {
height: 100%;
display: flex;
flex-direction: column;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #f5f7fa;
border-bottom: 1px solid #dcdfe6;
.language-selector {
min-width: 120px;
}
.editor-actions {
display: flex;
gap: 8px;
}
}
.editor-wrapper {
flex: 1;
min-height: 400px;
}
.editor-footer {
background: #f5f7fa;
border-top: 1px solid #dcdfe6;
padding: 4px 12px;
.status-bar {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #909399;
}
}
}
</style>
6. 性能优化策略
6.1 多级缓存架构深度优化
Redis缓存策略优化:
@Service
@Slf4j
public class AdvancedCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedissonClient redissonClient;
// 缓存键前缀
private static final String QUESTION_CACHE_PREFIX = "question:";
private static final String USER_CACHE_PREFIX = "user:";
private static final String EXAM_CACHE_PREFIX = "exam:";
private static final String LEADERBOARD_PREFIX = "leaderboard:";
/**
* 多级缓存获取:本地缓存 + Redis
*/
public <T> T getWithMultiLevelCache(String key, Class<T> clazz,
Supplier<T> loader, long expireTime) {
// 第一级:本地缓存(Caffeine)
T value = getFromLocalCache(key, clazz);
if (value != null) {
return value;
}
// 第二级:Redis分布式缓存
value = getFromRedis(key, clazz);
if (value != null) {
// 回填本地缓存
putToLocalCache(key, value);
return value;
}
// 第三级:数据库加载
return loadAndCache(key, clazz, loader, expireTime);
}
/**
* 批量缓存操作
*/
public <T> Map<String, T> multiGet(List<String> keys, Class<T> clazz) {
if (keys == null || keys.isEmpty()) {
return Collections.emptyMap();
}
List<Object> values = redisTemplate.opsForValue().multiGet(keys);
Map<String, T> result = new HashMap<>();
for (int i = 0; i < keys.size(); i++) {
if (values.get(i) != null) {
result.put(keys.get(i), clazz.cast(values.get(i)));
}
}
return result;
}
/**
* 批量缓存设置
*/
public <T> void multiSet(Map<String, T> keyValueMap, long expireTime) {
if (keyValueMap == null || keyValueMap.isEmpty()) {
return;
}
Map<String, Object> redisMap = new HashMap<>();
for (Map.Entry<String, T> entry : keyValueMap.entrySet()) {
redisMap.put(entry.getKey(), entry.getValue());
}
redisTemplate.opsForValue().multiSet(redisMap);
// 设置过期时间
for (String key : keyValueMap.keySet()) {
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
}
}
/**
* 分布式锁保护缓存击穿
*/
public <T> T getWithLock(String key, Class<T> clazz,
Supplier<T> loader, long expireTime) {
// 先尝试从缓存获取
T value = getFromRedis(key, clazz);
if (value != null) {
return value;
}
// 获取分布式锁
RLock lock = redissonClient.getLock("lock:" + key);
try {
// 尝试加锁,最多等待5秒,锁持有30秒
boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS);
if (locked) {
// 双重检查
value = getFromRedis(key, clazz);
if (value != null) {
return value;
}
// 加载数据
value = loader.get();
if (value != null) {
setToRedis(key, value, expireTime);
}
return value;
} else {
// 获取锁失败,返回空或默认值
log.warn("获取分布式锁失败: {}", key);
return null;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new CacheException("缓存获取被中断", e);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 缓存预热
*/
@Async
public void preheatCache() {
log.info("开始缓存预热...");
// 预热热门题目
preheatHotQuestions();
// 预热排行榜
preheatLeaderboards();
// 预热系统配置
preheatSystemConfig();
log.info("缓存预热完成");
}
private void preheatHotQuestions() {
List<Question> hotQuestions = questionService.getHotQuestions(100);
Map<String, Object> cacheMap = new HashMap<>();
for (Question question : hotQuestions) {
String key = QUESTION_CACHE_PREFIX + question.getQuestionId();
cacheMap.put(key, question);
}
multiSet(cacheMap, 30 * 60); // 30分钟
}
/**
* 缓存统计和监控
*/
public CacheStats getCacheStats() {
RMapCache<String, Object> statsMap = redissonClient.getMapCache("cache:stats");
long hitCount = statsMap.get("hitCount", Long.class);
long missCount = statsMap.get("missCount", Long.class);
long totalCount = hitCount + missCount;
double hitRate = totalCount > 0 ? (double) hitCount / totalCount : 0;
return new CacheStats(hitCount, missCount, hitRate);
}
/**
* 缓存清理策略
*/
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void cleanExpiredCache() {
log.info("开始清理过期缓存...");
// 扫描过期的缓存键
Set<String> keys = redisTemplate.keys("*");
int cleanedCount = 0;
for (String key : keys) {
Long ttl = redisTemplate.getExpire(key);
if (ttl != null && ttl < 0) {
// TTL为-1表示没有设置过期时间,-2表示键不存在
redisTemplate.delete(key);
cleanedCount++;
}
}
log.info("缓存清理完成,共清理 {} 个键", cleanedCount);
}
// 本地缓存(Caffeine)配置
@Bean
public Cache<String, Object> localCache() {
return Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats()
.build();
}
private <T> T getFromLocalCache(String key, Class<T> clazz) {
try {
Object value = localCache().getIfPresent(key);
return clazz.cast(value);
} catch (Exception e) {
log.warn("本地缓存获取失败: {}", key, e);
return null;
}
}
private <T> void putToLocalCache(String key, T value) {
try {
localCache().put(key, value);
} catch (Exception e) {
log.warn("本地缓存写入失败: {}", key, e);
}
}
private <T> T getFromRedis(String key, Class<T> clazz) {
try {
Object value = redisTemplate.opsForValue().get(key);
return value != null ? clazz.cast(value) : null;
} catch (Exception e) {
log.warn("Redis缓存获取失败: {}", key, e);
return null;
}
}
private <T> void setToRedis(String key, T value, long expireTime) {
try {
redisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.SECONDS);
} catch (Exception e) {
log.warn("Redis缓存写入失败: {}", key, e);
}
}
private <T> T loadAndCache(String key, Class<T> clazz,
Supplier<T> loader, long expireTime) {
T value = loader.get();
if (value != null) {
setToRedis(key, value, expireTime);
putToLocalCache(key, value);
}
return value;
}
}
6.2 数据库深度优化
高级查询优化:
-- 数据库性能优化配置
-- 1. 索引优化
CREATE INDEX idx_question_difficulty_status ON tb_question(difficulty, status);
CREATE INDEX idx_submission_user_question_status ON tb_submission(user_id, question_id, status);
CREATE INDEX idx_submission_create_time_desc ON tb_submission(create_time DESC);
CREATE INDEX idx_exam_status_times ON tb_exam(status, start_time, end_time);
-- 2. 分区表管理
-- 提交记录表按时间分区(每月一个分区)
ALTER TABLE tb_submission PARTITION BY RANGE (TO_DAYS(create_time)) (
PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')),
PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')),
PARTITION p202403 VALUES LESS THAN (TO_DAYS('2024-04-01')),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
-- 3. 查询性能监控
-- 慢查询日志配置
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 2;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
-- 4. 数据库连接池优化
-- application.yml 配置
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
connection-test-query: SELECT 1
MyBatis Plus查询优化:
// 高级查询服务
@Service
@Slf4j
public class AdvancedQueryService {
@Autowired
private QuestionMapper questionMapper;
@Autowired
private SubmissionMapper submissionMapper;
/**
* 分页查询优化 - 避免深度分页
*/
public PageResult<QuestionVO> searchQuestionsOptimized(QuestionQueryDTO query) {
// 使用游标分页替代传统分页
if (query.getPageNum() > 100) {
return searchQuestionsWithCursor(query);
}
// 传统分页
Page<Question> page = new Page<>(query.getPageNum(), query.getPageSize());
IPage<QuestionVO> result = questionMapper.selectQuestionPage(page, query);
return PageResult.of(result);
}
/**
* 游标分页查询
*/
private PageResult<QuestionVO> searchQuestionsWithCursor(QuestionQueryDTO query) {
Long cursor = query.getCursor(); // 游标值(最后一条记录的ID)
Integer size = query.getPageSize();
List<QuestionVO> questions = questionMapper.selectQuestionsWithCursor(cursor, size);
// 构建下一页游标
Long nextCursor = null;
if (questions.size() == size) {
nextCursor = questions.get(questions.size() - 1).getQuestionId();
}
return PageResult.withCursor(questions, nextCursor);
}
/**
* 批量插入优化
*/
@Transactional
public void batchInsertSubmissions(List<Submission> submissions) {
if (submissions == null || submissions.isEmpty()) {
return;
}
// 分批插入,避免单次插入数据量过大
int batchSize = 1000;
for (int i = 0; i < submissions.size(); i += batchSize) {
int end = Math.min(i + batchSize, submissions.size());
List<Submission> batch = submissions.subList(i, end);
submissionMapper.insertBatchSomeColumn(batch);
}
}
/**
* 查询结果缓存
*/
@Cacheable(value = "question_search", key = "#query.hashCode()")
public PageResult<QuestionVO> searchQuestionsCached(QuestionQueryDTO query) {
return searchQuestionsOptimized(query);
}
/**
* 统计查询优化
*/
public QuestionStatistics getQuestionStatistics(Long questionId) {
// 使用单个查询获取所有统计信息
return questionMapper.selectQuestionStatistics(questionId);
}
}
// 自定义SQL注入器
@Component
public class CustomSqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
// 添加自定义方法
methodList.add(new InsertBatchSomeColumn());
methodList.add(new SelectWithCursor());
methodList.add(new UpdateOptimisticLock());
return methodList;
}
}
// 乐观锁更新方法
public class UpdateOptimisticLock extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
if (!tableInfo.isWithVersion()) {
return null;
}
String sql = String.format("<script>UPDATE %s %s WHERE %s=#{%s} AND %s=#{%s}</script>",
tableInfo.getTableName(),
sqlSet(tableInfo.isWithLogicDelete(), false, tableInfo, false, "et", "et."),
tableInfo.getKeyColumn(), tableInfo.getKeyProperty(),
tableInfo.getVersionColumn(), tableInfo.getVersionProperty());
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addUpdateMappedStatement(mapperClass, modelClass, "updateByIdWithVersion", sqlSource);
}
}
7. 部署与运维体系
7.1 容器化部署深度设计
Docker多环境配置:
# docker-compose.yml - 生产环境配置
version: '3.8'
x-common-variables: &common-variables
SPRING_PROFILES_ACTIVE: prod
NACOS_HOST: nacos-server
REDIS_HOST: redis-server
MYSQL_HOST: mysql-server
RABBITMQ_HOST: rabbitmq-server
services:
# 基础设施服务
mysql-server:
image: mysql:8.0
container_name: oj-mysql
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: bitoj_prod
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./config/mysql/conf.d:/etc/mysql/conf.d
- ./backup/mysql:/backup
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --innodb-buffer-pool-size=2G
- --innodb-log-file-size=256M
- --max-connections=1000
restart: unless-stopped
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 10s
retries: 5
redis-server:
image: redis:6.2-alpine
container_name: oj-redis
command:
- redis-server
- --requirepass ${REDIS_PASSWORD}
- --maxmemory 1gb
- --maxmemory-policy allkeys-lru
ports:
- "6379:6379"
volumes:
- redis_data:/data
- ./config/redis/redis.conf:/etc/redis/redis.conf
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
timeout: 5s
retries: 5
nacos-server:
image: nacos/nacos-server:v2.2.3
container_name: oj-nacos
environment:
- MODE=cluster
- SPRING_DATASOURCE_PLATFORM=mysql
- MYSQL_SERVICE_HOST=mysql-server
- MYSQL_SERVICE_DB_NAME=bitoj_nacos
- MYSQL_SERVICE_USER=${MYSQL_USER}
- MYSQL_SERVICE_PASSWORD=${MYSQL_PASSWORD}
- NACOS_SERVERS=nacos-server:8848
- NACOS_APPLICATION_PORT=8848
ports:
- "8848:8848"
volumes:
- nacos_data:/home/nacos/data
- nacos_logs:/home/nacos/logs
restart: unless-stopped
depends_on:
mysql-server:
condition: service_healthy
rabbitmq-server:
image: rabbitmq:3.8-management
container_name: oj-rabbitmq
environment:
- RABBITMQ_DEFAULT_USER=${RABBITMQ_USER}
- RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD}
- RABBITMQ_DEFAULT_VHOST=/
ports:
- "5672:5672"
- "15672:15672"
volumes:
- rabbitmq_data:/var/lib/rabbitmq
- rabbitmq_logs:/var/log/rabbitmq
restart: unless-stopped
healthcheck:
test: ["CMD", "rabbitmqctl", "status"]
timeout: 10s
retries: 5
# 业务微服务
gateway-service:
build:
context: ./oj-gateway
dockerfile: Dockerfile.prod
container_name: oj-gateway
environment:
<<: *common-variables
JAVA_OPTS: "-Xmx512m -Xms256m -XX:+UseG1GC -Djava.security.egd=file:/dev/./urandom"
ports:
- "19090:19090"
depends_on:
nacos-server:
condition: service_started
redis-server:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:19090/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
user-service:
build:
context: ./oj-user
dockerfile: Dockerfile.prod
container_name: oj-user
environment:
<<: *common-variables
JAVA_OPTS: "-Xmx1g -Xms512m -XX:+UseG1GC"
deploy:
replicas: 2
depends_on:
nacos-server:
condition: service_started
restart: unless-stopped
question-service:
build:
context: ./oj-question
dockerfile: Dockerfile.prod
container_name: oj-question
environment:
<<: *common-variables
JAVA_OPTS: "-Xmx1g -Xms512m -XX:+UseG1GC"
deploy:
replicas: 2
depends_on:
nacos-server:
condition: service_started
restart: unless-stopped
judge-service:
build:
context: ./oj-judge
dockerfile: Dockerfile.prod
container_name: oj-judge
environment:
<<: *common-variables
JAVA_OPTS: "-Xmx2g -Xms1g -XX:+UseG1GC"
DOCKER_HOST: unix:///var/run/docker.sock
volumes:
- /var/run/docker.sock:/var/run/docker.sock
deploy:
replicas: 3
depends_on:
nacos-server:
condition: service_started
rabbitmq-server:
condition: service_healthy
restart: unless-stopped
# 前端服务
frontend-service:
build:
context: ./oj-frontend
dockerfile: Dockerfile.prod
container_name: oj-frontend
ports:
- "80:80"
- "443:443"
volumes:
- ./config/nginx/nginx.conf:/etc/nginx/nginx.conf
- ./config/nginx/conf.d:/etc/nginx/conf.d
- ssl_certs:/etc/nginx/ssl
depends_on:
- gateway-service
restart: unless-stopped
volumes:
mysql_data:
redis_data:
nacos_data:
nacos_logs:
rabbitmq_data:
rabbitmq_logs:
ssl_certs:
networks:
default:
name: oj-network
driver: bridge
Kubernetes生产部署:
# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: online-judge
labels:
name: online-judge
---
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: online-judge
data:
application-prod.yml: |
spring:
datasource:
url: jdbc:mysql://mysql-service:3306/bitoj_prod?useUnicode=true&characterEncoding=utf8&useSSL=false
username: ${MYSQL_USER}
password: ${MYSQL_PASSWORD}
redis:
host: redis-service
password: ${REDIS_PASSWORD}
cloud:
nacos:
discovery:
server-addr: nacos-service:8848
config:
server-addr: nacos-service:8848
rabbitmq:
host: rabbitmq-service
username: ${RABBITMQ_USER}
password: ${RABBITMQ_PASSWORD}
---
# k8s/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
namespace: online-judge
type: Opaque
data:
mysql-user: <base64-encoded>
mysql-password: <base64-encoded>
redis-password: <base64-encoded>
rabbitmq-user: <base64-encoded>
rabbitmq-password: <base64-encoded>
---
# k8s/gateway-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: gateway-service
namespace: online-judge
labels:
app: gateway-service
spec:
replicas: 2
selector:
matchLabels:
app: gateway-service
template:
metadata:
labels:
app: gateway-service
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "19090"
prometheus.io/path: "/actuator/prometheus"
spec:
containers:
- name: gateway-service
image: registry.example.com/oj-gateway:1.0.0
ports:
- containerPort: 19090
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: JAVA_OPTS
value: "-Xmx512m -Xms256m -XX:+UseG1GC -Djava.security.egd=file:/dev/./urandom"
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /actuator/health
port: 19090
initialDelaySeconds: 60
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health
port: 19090
initialDelaySeconds: 30
periodSeconds: 5
volumeMounts:
- name: config-volume
mountPath: /app/config
volumes:
- name: config-volume
configMap:
name: app-config
---
# k8s/gateway-service.yaml
apiVersion: v1
kind: Service
metadata:
name: gateway-service
namespace: online-judge
labels:
app: gateway-service
spec:
selector:
app: gateway-service
ports:
- port: 19090
targetPort: 19090
name: http
type: ClusterIP
---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: oj-ingress
namespace: online-judge
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
spec:
tls:
- hosts:
- oj.example.com
secretName: oj-tls-secret
rules:
- host: oj.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80
- path: /api
pathType: Prefix
backend:
service:
name: gateway-service
port:
number: 19090
---
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: gateway-hpa
namespace: online-judge
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: gateway-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
7.2 监控与日志体系
Prometheus + Grafana监控:
# monitoring/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "alert_rules.yml"
scrape_configs:
- job_name: 'online-judge'
metrics_path: '/actuator/prometheus'
static_configs:
- targets:
- 'gateway-service:19090'
- 'user-service:8080'
- 'question-service:8080'
- 'judge-service:8080'
relabel_configs:
- source_labels: [__address__]
target_label: instance
regex: '(.*):\d+'
replacement: '${1}'
- job_name: 'rabbitmq'
static_configs:
- targets: ['rabbitmq-service:15672']
metrics_path: '/api/metrics'
basic_auth:
username: '${RABBITMQ_USER}'
password: '${RABBITMQ_PASSWORD}'
- job_name: 'mysql'
static_configs:
- targets: ['mysql-service:9104']
metrics_path: '/metrics'
- job_name: 'redis'
static_configs:
- targets: ['redis-service:9121']
metrics_path: '/metrics'
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
Spring Boot Actuator深度配置:
// 监控配置类
@Configuration
@EnableConfigurationProperties(value = {EndpointProperties.class, WebEndpointProperties.class})
public class MonitoringConfiguration {
@Bean
@ConfigurationProperties("management.endpoint.health")
public HealthEndpointProperties healthEndpointProperties() {
return new HealthEndpointProperties();
}
@Bean
public HealthContributorRegistry healthContributorRegistry(
ObjectProvider<HealthIndicator> healthIndicators,
ObjectProvider<HealthContributor> healthContributors) {
AutoConfiguredHealthContributorRegistry registry =
new AutoConfiguredHealthContributorRegistry();
healthIndicators.orderedStream().forEach(registry::registerContributor);
healthContributors.orderedStream().forEach(registry::registerContributor);
return registry;
}
@Bean
public HealthEndpoint healthEndpoint(HealthContributorRegistry registry,
HealthEndpointProperties properties) {
return new HealthEndpoint(registry, properties.getShowDetails(),
properties.getShowComponents());
}
// 自定义健康检查
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
@Autowired
private DataSource dataSource;
@Override
public Health health() {
try (Connection connection = dataSource.getConnection()) {
DatabaseMetaData metaData = connection.getMetaData();
String databaseName = metaData.getDatabaseProductName();
String databaseVersion = metaData.getDatabaseProductVersion();
// 检查数据库连接池状态
if (dataSource instanceof HikariDataSource) {
HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
long activeConnections = hikariDataSource.getHikariPoolMXBean().getActiveConnections();
long idleConnections = hikariDataSource.getHikariPoolMXBean().getIdleConnections();
long totalConnections = hikariDataSource.getHikariPoolMXBean().getTotalConnections();
return Health.up()
.withDetail("database", databaseName)
.withDetail("version", databaseVersion)
.withDetail("activeConnections", activeConnections)
.withDetail("idleConnections", idleConnections)
.withDetail("totalConnections", totalConnections)
.build();
}
return Health.up()
.withDetail("database", databaseName)
.withDetail("version", databaseVersion)
.build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}
@Component
public class RedisHealthIndicator implements HealthIndicator {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public Health health() {
try {
// 测试Redis连接
String testKey = "health:check:" + System.currentTimeMillis();
redisTemplate.opsForValue().set(testKey, "test", Duration.ofSeconds(10));
String value = (String) redisTemplate.opsForValue().get(testKey);
redisTemplate.delete(testKey);
if ("test".equals(value)) {
return Health.up()
.withDetail("version", getRedisVersion())
.withDetail("memory", getRedisMemoryInfo())
.build();
} else {
return Health.down().withDetail("error", "Redis test failed").build();
}
} catch (Exception e) {
return Health.down(e).build();
}
}
private String getRedisVersion() {
try {
Properties info = redisTemplate.getRequiredConnectionFactory()
.getConnection().info("server");
return info.getProperty("redis_version");
} catch (Exception e) {
return "unknown";
}
}
private String getRedisMemoryInfo() {
try {
Properties info = redisTemplate.getRequiredConnectionFactory()
.getConnection().info("memory");
return info.getProperty("used_memory_human") + " / " +
info.getProperty("maxmemory_human");
} catch (Exception e) {
return "unknown";
}
}
}
// 自定义业务指标
@Component
public class BusinessMetrics {
private final Counter submissionCounter;
private final Counter acceptedCounter;
private final Gauge acceptanceRate;
private final Timer judgeTimer;
private final DistributionSummary codeSizeSummary;
public BusinessMetrics(MeterRegistry registry) {
this.submissionCounter = Counter.builder("oj.submission.total")
.description("Total number of code submissions")
.register(registry);
this.acceptedCounter = Counter.builder("oj.submission.accepted")
.description("Number of accepted submissions")
.register(registry);
this.acceptanceRate = Gauge.builder("oj.submission.acceptance.rate")
.description("Code acceptance rate")
.register(registry);
this.judgeTimer = Timer.builder("oj.judge.duration")
.description("Time taken for code judgment")
.publishPercentiles(0.5, 0.95, 0.99)
.register(registry);
this.codeSizeSummary = DistributionSummary.builder("oj.code.size")
.description("Size of submitted code")
.baseUnit("characters")
.register(registry);
}
public void recordSubmission(boolean accepted, long codeSize, long judgeTime) {
submissionCounter.increment();
if (accepted) {
acceptedCounter.increment();
}
// 更新通过率
double rate = (double) acceptedCounter.count() / submissionCounter.count();
acceptanceRate.set(rate);
// 记录执行时间
judgeTimer.record(judgeTime, TimeUnit.MILLISECONDS);
// 记录代码大小
codeSizeSummary.record(codeSize);
}
}
}
ELK日志收集配置:
# docker-compose.logging.yml
version: '3.8'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0
container_name: elasticsearch
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- elasticsearch_data:/usr/share/elasticsearch/data
ports:
- "9200:9200"
networks:
- logging
logstash:
image: docker.elastic.co/logstash/logstash:7.17.0
container_name: logstash
volumes:
- ./config/logstash/logstash.conf:/usr/share/logstash/pipeline/logstash.conf
- ./config/logstash/logstash.yml:/usr/share/logstash/config/logstash.yml
ports:
- "5044:5044"
- "5000:5000/tcp"
- "5000:5000/udp"
- "9600:9600"
environment:
LS_JAVA_OPTS: "-Xmx256m -Xms256m"
depends_on:
- elasticsearch
networks:
- logging
kibana:
image: docker.elastic.co/kibana/kibana:7.17.0
container_name: kibana
ports:
- "5601:5601"
environment:
ELASTICSEARCH_HOSTS: http://elasticsearch:9200
depends_on:
- elasticsearch
networks:
- logging
filebeat:
image: docker.elastic.co/beats/filebeat:7.17.0
container_name: filebeat
volumes:
- ./config/filebeat/filebeat.yml:/usr/share/filebeat/filebeat.yml
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
- logstash
networks:
- logging
volumes:
elasticsearch_data:
networks:
logging:
driver: bridge
# docker-compose.logging.yml
version: '3.8'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0
container_name: elasticsearch
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- elasticsearch_data:/usr/share/elasticsearch/data
ports:
- "9200:9200"
networks:
- logging
logstash:
image: docker.elastic.co/logstash/logstash:7.17.0
container_name: logstash
volumes:
- ./config/logstash/logstash.conf:/usr/share/logstash/pipeline/logstash.conf
- ./config/logstash/logstash.yml:/usr/share/logstash/config/logstash.yml
ports:
- "5044:5044"
- "5000:5000/tcp"
- "5000:5000/udp"
- "9600:9600"
environment:
LS_JAVA_OPTS: "-Xmx256m -Xms256m"
depends_on:
- elasticsearch
networks:
- logging
kibana:
image: docker.elastic.co/kibana/kibana:7.17.0
container_name: kibana
ports:
- "5601:5601"
environment:
ELASTICSEARCH_HOSTS: http://elasticsearch:9200
depends_on:
- elasticsearch
networks:
- logging
filebeat:
image: docker.elastic.co/beats/filebeat:7.17.0
container_name: filebeat
volumes:
- ./config/filebeat/filebeat.yml:/usr/share/filebeat/filebeat.yml
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
- logstash
networks:
- logging
volumes:
elasticsearch_data:
networks:
logging:
driver: bridge
# config/logstash/logstash.conf
input {
beats {
port => 5044
}
}
filter {
# 解析Docker日志
if [docker][container][labels][com_docker_compose_project] == "online-judge" {
grok {
match => { "message" => "\[%{TIMESTAMP_ISO8601:timestamp}\] \[%{DATA:thread}\] %{LOGLEVEL:loglevel} %{DATA:logger} - \[%{DATA:method},%{NUMBER:line}\] - %{GREEDYDATA:message}" }
}
# 解析JSON格式的日志
if [message] =~ /^{.*}$/ {
json {
source => "message"
target => "json_content"
}
}
# 添加业务标签
if [docker][container][labels][com_docker_compose_service] {
mutate {
add_field => {
"service" => "%{[docker][container][labels][com_docker_compose_service]}"
"environment" => "production"
}
}
}
# 日期处理
date {
match => [ "timestamp", "ISO8601" ]
target => "@timestamp"
}
}
}
output {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "online-judge-%{+YYYY.MM.dd}"
}
# 开发环境同时输出到控制台
if [environment] == "development" {
stdout { codec => rubydebug }
}
}
8. 安全防护体系
8.1 全面的安全防护
Web安全深度配置:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private TokenService tokenService;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// CSRF配置
.csrf().disable()
// 会话管理
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 异常处理
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint())
.accessDeniedHandler(accessDeniedHandler())
.and()
// 权限配置
.authorizeRequests()
// 公开接口
.antMatchers(
"/auth/login",
"/auth/register",
"/auth/refresh",
"/public/**",
"/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-resources/**",
"/webjars/**"
).permitAll()
// 用户接口
.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
// 管理员接口
.antMatchers("/admin/**").hasRole("ADMIN")
// 需要认证的接口
.anyRequest().authenticated()
.and()
// JWT过滤器
.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class)
// 安全头配置
.headers()
.contentSecurityPolicy("default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'")
.and()
.httpStrictTransportSecurity()
.includeSubDomains(true)
.maxAgeInSeconds(31536000)
.and()
.frameOptions().deny()
.xssProtection().block(true);
}
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
return new JwtAuthenticationTokenFilter(tokenService);
}
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return (request, response, authException) -> {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
R<String> result = R.fail(ResultCode.FAILED_UNAUTHORIZED.getCode(),
"认证失败: " + authException.getMessage());
response.getWriter().write(JSON.toJSONString(result));
};
}
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return (request, response, accessDeniedException) -> {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.FORBIDDEN.value());
R<String> result = R.fail(ResultCode.FAILED_FORBIDDEN.getCode(),
"权限不足: " + accessDeniedException.getMessage());
response.getWriter().write(JSON.toJSONString(result));
};
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
// 安全审计
@Bean
public AuditorAware<Long> auditorAware() {
return new SecurityAuditorAware();
}
}
// 安全审计组件
@Component
public class SecurityAuditorAware implements AuditorAware<Long> {
@Override
public Optional<Long> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return Optional.empty();
}
if (authentication.getPrincipal() instanceof LoginUser) {
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
return Optional.of(loginUser.getUserId());
}
return Optional.empty();
}
}
// 请求限流配置
@Configuration
public class RateLimitConfiguration {
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
@Bean
public FilterRegistrationBean<SentinelFilter> sentinelFilter() {
FilterRegistrationBean<SentinelFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new SentinelFilter());
registration.addUrlPatterns("/*");
registration.setName("sentinelFilter");
registration.setOrder(1);
return registration;
}
}
// 自定义Sentinel过滤器
public class SentinelFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String path = httpRequest.getRequestURI();
String method = httpRequest.getMethod();
// 限流规则检查
if (!passRateLimit(path, method)) {
((HttpServletResponse) response).setStatus(429);
response.getWriter().write("{\"code\":429,\"message\":\"请求过于频繁,请稍后重试\"}");
return;
}
chain.doFilter(request, response);
}
private boolean passRateLimit(String path, String method) {
// 实现具体的限流逻辑
// 可以根据路径、方法、用户等进行限流
return true;
}
}
代码安全沙箱增强:
@Service
public class EnhancedCodeSandbox {
private static final Set<String> FORBIDDEN_SYSTEM_PROPERTIES = Set.of(
"java.home", "user.dir", "user.home", "java.io.tmpdir"
);
/**
* 深度代码安全检查
*/
public SecurityCheckResult deepSecurityCheck(String code, String language) {
SecurityCheckResult result = new SecurityCheckResult();
// 1. 基础安全检查
result.merge(basicSecurityCheck(code, language));
// 2. 语言特定检查
result.merge(languageSpecificCheck(code, language));
// 3. 复杂度检查
result.merge(complexityCheck(code));
// 4. 资源使用预测
result.merge(resourceUsagePrediction(code, language));
return result;
}
private SecurityCheckResult basicSecurityCheck(String code, String language) {
SecurityCheckResult result = new SecurityCheckResult();
// 检查危险系统调用
checkDangerousSystemCalls(code, result);
// 检查文件操作
checkFileOperations(code, result);
// 检查网络操作
checkNetworkOperations(code, result);
// 检查反射操作
checkReflectionOperations(code, result);
// 检查线程操作
checkThreadOperations(code, result);
return result;
}
private void checkDangerousSystemCalls(String code, SecurityCheckResult result) {
Pattern dangerousPatterns = Pattern.compile(
"Runtime\\.getRuntime\\(\\)\\.exec\\s*\\(|" +
"System\\.exit\\s*\\(|" +
"ProcessBuilder|" +
"UNSAFE|" +
"Unsafe\\.getUnsafe|" +
"sun\\.misc|" +
"jdk\\.internal",
Pattern.CASE_INSENSITIVE
);
Matcher matcher = dangerousPatterns.matcher(code);
while (matcher.find()) {
result.addRisk(new SecurityRisk(
"DANGEROUS_SYSTEM_CALL",
"检测到危险系统调用: " + matcher.group(),
SecurityLevel.HIGH
));
}
}
private void checkFileOperations(String code, SecurityCheckResult result) {
Pattern filePatterns = Pattern.compile(
"File\\.|" +
"FileInputStream|" +
"FileOutputStream|" +
"Files\\.|" +
"Paths\\.get|" +
"RandomAccessFile",
Pattern.CASE_INSENSITIVE
);
Matcher matcher = filePatterns.matcher(code);
while (matcher.find()) {
result.addRisk(new SecurityRisk(
"FILE_OPERATION",
"检测到文件操作: " + matcher.group(),
SecurityLevel.MEDIUM
));
}
}
/**
* 代码复杂度分析
*/
private SecurityCheckResult complexityCheck(String code) {
SecurityCheckResult result = new SecurityCheckResult();
// 计算圈复杂度
int cyclomaticComplexity = calculateCyclomaticComplexity(code);
if (cyclomaticComplexity > 20) {
result.addRisk(new SecurityRisk(
"HIGH_COMPLEXITY",
"代码圈复杂度过高: " + cyclomaticComplexity,
SecurityLevel.MEDIUM
));
}
// 检查嵌套深度
int maxNestingDepth = calculateMaxNestingDepth(code);
if (maxNestingDepth > 5) {
result.addRisk(new SecurityRisk(
"DEEP_NESTING",
"代码嵌套深度过大: " + maxNestingDepth,
SecurityLevel.MEDIUM
));
}
// 检查递归调用
if (hasDeepRecursion(code)) {
result.addRisk(new SecurityRisk(
"DEEP_RECURSION",
"检测到深层递归调用",
SecurityLevel.HIGH
));
}
return result;
}
/**
* 资源使用预测
*/
private SecurityCheckResult resourceUsagePrediction(String code, String language) {
SecurityCheckResult result = new SecurityCheckResult();
// 预测内存使用
long estimatedMemory = estimateMemoryUsage(code, language);
if (estimatedMemory > 100 * 1024 * 1024) { // 100MB
result.addRisk(new SecurityRisk(
"HIGH_MEMORY_USAGE",
"预测内存使用过高: " + (estimatedMemory / 1024 / 1024) + "MB",
SecurityLevel.MEDIUM
));
}
// 预测执行时间
long estimatedTime = estimateExecutionTime(code, language);
if (estimatedTime > 5000) { // 5秒
result.addRisk(new SecurityRisk(
"LONG_EXECUTION_TIME",
"预测执行时间过长: " + estimatedTime + "ms",
SecurityLevel.MEDIUM
));
}
return result;
}
// 安全执行环境
public ExecuteResult executeInSecureEnvironment(ExecuteRequest request) {
// 创建安全策略文件
String policyFile = createSecurityPolicyFile(request.getLanguage());
// 使用SecurityManager执行代码
System.setSecurityManager(new OJSecurityManager());
try {
return doExecuteWithSecurityManager(request, policyFile);
} finally {
System.setSecurityManager(null);
}
}
// 自定义SecurityManager
public static class OJSecurityManager extends SecurityManager {
@Override
public void checkExec(String cmd) {
throw new SecurityException("执行系统命令被禁止");
}
@Override
public void checkRead(String file) {
// 只允许读取特定目录的文件
if (!file.startsWith("/tmp/oj/")) {
throw new SecurityException("文件读取被禁止: " + file);
}
}
@Override
public void checkWrite(String file) {
// 只允许写入特定目录的文件
if (!file.startsWith("/tmp/oj/")) {
throw new SecurityException("文件写入被禁止: " + file);
}
}
@Override
public void checkConnect(String host, int port) {
throw new SecurityException("网络连接被禁止");
}
@Override
public void checkCreateClassLoader() {
throw new SecurityException("创建类加载器被禁止");
}
@Override
public void checkExit(int status) {
throw new SecurityException("退出虚拟机被禁止");
}
}
}
9. 项目总结与展望
9.1 技术架构总结
架构演进历程:

技术决策回顾:
| 技术领域 | 决策内容 | 效果评估 | 改进方向 |
|---|---|---|---|
| 微服务框架 | Spring Cloud Alibaba | 开发效率高,生态完善 | 考虑Service Mesh |
| 数据库 | MySQL + 分库分表 | 满足当前性能需求 | 引入时序数据库 |
| 缓存 | Redis集群 | 性能提升明显 | 增加本地缓存 |
| 消息队列 | RabbitMQ | 稳定可靠 | 考虑Kafka |
| 搜索 | Elasticsearch | 搜索性能优秀 | 优化索引策略 |
9.2 性能指标达成
系统性能基准:
// 性能测试报告
public class PerformanceReport {
// 并发处理能力
private int maxConcurrentUsers = 5000;
private double throughput = 1200; // 请求/秒
private double averageResponseTime = 85; // 毫秒
// 代码判题性能
private int judgeThroughput = 200; // 判题/分钟
private double judgeSuccessRate = 99.8; // %
// 系统可用性
private double availability = 99.95; // %
private double errorRate = 0.02; // %
// 资源利用率
private double cpuUtilization = 65; // %
private double memoryUtilization = 70; // %
private double diskUtilization = 45; // %
}
9.3 业务价值实现
教育价值体现:
-
学习路径优化:基于用户行为数据的个性化推荐
-
技能评估:多维度的编程能力评估体系
-
竞赛体系:完整的竞赛生命周期管理
-
社区互动:代码评审、讨论区等社交功能
商业价值实现:
-
技术面试:为企业提供技术人才评估解决方案
-
教育培训:为教育机构提供在线编程教学平台
-
技能认证:建立行业认可的编程技能认证体系
9.4 未来演进方向
技术演进规划:

具体演进计划:
-
短期(6个月)
-
性能优化:缓存策略优化,数据库查询优化
-
体验提升:前端性能优化,移动端适配
-
监控完善:APM全链路监控,智能告警
-
-
中期(1-2年)
-
云原生:全面转向Kubernetes,服务网格
-
AI集成:智能题目推荐,代码自动评分
-
多租户:支持SaaS化部署,租户隔离
-
-
长期(2-3年)
-
平台化:开放API,第三方应用集成
-
国际化:多语言支持,全球部署
-
生态建设:开发者社区,插件市场
-
10. 监控与告警系统
10.1 全链路监控
分布式追踪配置:
// SkyWalking配置
@Configuration
public class TracingConfiguration {
@Bean
public Tracing tracing() {
return Tracing.newBuilder()
.localServiceName("online-judge")
.spanReporter(spanReporter())
.currentTraceContext(currentTraceContext())
.build();
}
@Bean
public SpanReporter spanReporter() {
return new ZipkinReporter();
}
@Bean
public CurrentTraceContext currentTraceContext() {
return CurrentTraceContext.Default.create();
}
@Bean
public TracingFilter tracingFilter() {
return new TracingFilter(tracing());
}
}
// 自定义业务追踪
@Aspect
@Component
@Slf4j
public class BusinessTracingAspect {
private final Tracer tracer;
public BusinessTracingAspect(Tracer tracer) {
this.tracer = tracer;
}
@Around("@annotation(BusinessTrace)")
public Object traceBusinessMethod(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
ScopedSpan span = tracer.startScopedSpan(className + "." + methodName);
try {
span.tag("class", className);
span.tag("method", methodName);
Object result = joinPoint.proceed();
span.finish();
return result;
} catch (Exception e) {
span.error(e);
span.finish();
throw e;
}
}
}
// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BusinessTrace {
String value() default "";
}
10.2 智能告警系统
告警规则配置:
# alert_rules.yml
groups:
- name: online-judge-alerts
rules:
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
for: 2m
labels:
severity: critical
service: online-judge
annotations:
summary: "高错误率告警"
description: "错误率超过10%,当前值: {{ $value }}"
- alert: HighResponseTime
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1
for: 3m
labels:
severity: warning
service: online-judge
annotations:
summary: "高响应时间告警"
description: "95%分位响应时间超过1秒,当前值: {{ $value }}s"
- alert: ServiceDown
expr: up{job=~".*"} == 0
for: 1m
labels:
severity: critical
service: online-judge
annotations:
summary: "服务下线告警"
description: "服务 {{ $labels.instance }} 已下线"
- alert: HighMemoryUsage
expr: container_memory_usage_bytes{container!="POD"} / container_spec_memory_limit_bytes > 0.8
for: 5m
labels:
severity: warning
service: online-judge
annotations:
summary: "高内存使用告警"
description: "内存使用率超过80%,当前值: {{ $value }}"
- alert: JudgeQueueBacklog
expr: rabbitmq_queue_messages_ready{queue="judge_queue"} > 1000
for: 2m
labels:
severity: warning
service: online-judge
annotations:
summary: "判题队列积压告警"
description: "判题队列积压超过1000,当前值: {{ $value }}"
告警通知配置:
@Service
@Slf4j
public class AlertNotificationService {
@Autowired
private MessageService messageService;
@Autowired
private EmailService emailService;
@Autowired
private SmsService smsService;
@EventListener
public void handleAlertEvent(AlertEvent event) {
log.info("处理告警事件: {}", event);
// 根据告警级别发送不同通知
switch (event.getSeverity()) {
case CRITICAL:
sendCriticalAlert(event);
break;
case WARNING:
sendWarningAlert(event);
break;
case INFO:
sendInfoAlert(event);
break;
}
// 记录告警到数据库
saveAlertRecord(event);
}
private void sendCriticalAlert(AlertEvent event) {
// 发送短信通知
smsService.sendCriticalAlert(event);
// 发送邮件通知
emailService.sendCriticalAlert(event);
// 发送系统通知
messageService.sendSystemAlert(event);
}
private void sendWarningAlert(AlertEvent event) {
// 发送邮件通知
emailService.sendWarningAlert(event);
// 发送系统通知
messageService.sendSystemAlert(event);
}
private void sendInfoAlert(AlertEvent event) {
// 发送系统通知
messageService.sendSystemAlert(event);
}
private void saveAlertRecord(AlertEvent event) {
AlertRecord record = AlertRecord.builder()
.alertName(event.getAlertName())
.severity(event.getSeverity())
.description(event.getDescription())
.startTime(event.getStartTime())
.endTime(event.getEndTime())
.status(AlertStatus.ACTIVE)
.build();
alertRecordRepository.save(record);
}
// 告警自动恢复检测
@Scheduled(fixedRate = 60000) // 每分钟检查一次
public void checkAlertRecovery() {
List<AlertRecord> activeAlerts = alertRecordRepository.findByStatus(AlertStatus.ACTIVE);
for (AlertRecord alert : activeAlerts) {
if (isAlertRecovered(alert)) {
alert.setStatus(AlertStatus.RESOLVED);
alert.setEndTime(LocalDateTime.now());
alertRecordRepository.save(alert);
// 发送恢复通知
sendRecoveryNotification(alert);
}
}
}
}
11. 持续集成与部署
11.1 GitLab CI/CD流水线
# .gitlab-ci.yml
stages:
- test
- build
- security-scan
- deploy
variables:
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
cache:
paths:
- .m2/repository/
- target/
# 单元测试
unit-test:
stage: test
image: maven:3.8-openjdk-17
script:
- mvn clean test
- mvn jacoco:report
artifacts:
paths:
- target/site/jacoco/
reports:
junit:
- target/surefire-reports/TEST-*.xml
only:
- merge_requests
- main
- develop
# 集成测试
integration-test:
stage: test
image: maven:3.8-openjdk-17
services:
- mysql:8.0
- redis:6.2
- rabbitmq:3.8-management
variables:
MYSQL_DATABASE: test_db
MYSQL_ROOT_PASSWORD: test
REDIS_PASSWORD: test
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
script:
- mvn verify -P integration-test
only:
- merge_requests
- main
# 代码质量检查
sonarqube-check:
stage: test
image: maven:3.8-openjdk-17
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
GIT_DEPTH: "0"
cache:
paths:
- .sonar/cache
script:
- mvn sonar:sonar -Dsonar.projectKey=online-judge
allow_failure: true
only:
- merge_requests
- main
# 安全扫描
security-scan:
stage: security-scan
image:
name: aquasec/trivy:latest
entrypoint: [""]
variables:
TRIVY_NO_PROGRESS: "true"
script:
- trivy fs --severity HIGH,CRITICAL --exit-code 1 .
- trivy config --severity HIGH,CRITICAL --exit-code 1 .
allow_failure: false
only:
- merge_requests
- main
# Docker镜像构建
build-backend:
stage: build
image: docker:20.10
services:
- docker:20.10-dind
variables:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- |
for service in gateway user question judge; do
docker build -t $CI_REGISTRY_IMAGE/$service:${CI_COMMIT_SHA} -f $service/Dockerfile.prod $service
docker push $CI_REGISTRY_IMAGE/$service:${CI_COMMIT_SHA}
done
only:
- main
# 部署到开发环境
deploy-dev:
stage: deploy
image:
name: bitnami/kubectl:latest
entrypoint: [""]
script:
- kubectl config use-context dev-cluster
- |
for service in gateway user question judge; do
kubectl set image deployment/$service-service $service=$CI_REGISTRY_IMAGE/$service:${CI_COMMIT_SHA} -n online-judge-dev
done
environment:
name: development
url: https://dev-oj.example.com
only:
- develop
# 部署到生产环境
deploy-prod:
stage: deploy
image:
name: bitnami/kubectl:latest
entrypoint: [""]
before_script:
- echo $KUBECONFIG | base64 -d > kubeconfig
- export KUBECONFIG=kubeconfig
script:
- kubectl config use-context prod-cluster
- |
for service in gateway user question judge; do
kubectl set image deployment/$service-service $service=$CI_REGISTRY_IMAGE/$service:${CI_COMMIT_SHA} -n online-judge
done
- kubectl rollout status deployment/gateway-service -n online-judge --timeout=300s
environment:
name: production
url: https://oj.example.com
when: manual
only:
- main
11.2 自动化测试策略
测试金字塔实现:
// 单元测试示例
@ExtendWith(MockitoExtension.class)
class QuestionServiceTest {
@Mock
private QuestionRepository questionRepository;
@Mock
private CacheService cacheService;
@InjectMocks
private QuestionService questionService;
@Test
void shouldReturnQuestionWhenExists() {
// Given
Long questionId = 1L;
Question question = createTestQuestion(questionId);
when(questionRepository.findById(questionId)).thenReturn(Optional.of(question));
when(cacheService.get(anyString(), eq(Question.class))).thenReturn(null);
// When
Question result = questionService.getQuestionById(questionId);
// Then
assertThat(result).isNotNull();
assertThat(result.getId()).isEqualTo(questionId);
verify(questionRepository).findById(questionId);
verify(cacheService).put(anyString(), eq(question), anyLong());
}
@Test
void shouldThrowExceptionWhenQuestionNotFound() {
// Given
Long questionId = 999L;
when(questionRepository.findById(questionId)).thenReturn(Optional.empty());
// When & Then
assertThatThrownBy(() -> questionService.getQuestionById(questionId))
.isInstanceOf(QuestionNotFoundException.class)
.hasMessage("题目不存在: " + questionId);
}
}
// 集成测试示例
@SpringBootTest
@ActiveProfiles("test")
@Testcontainers
class QuestionServiceIntegrationTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("test_db")
.withUsername("test")
.withPassword("test");
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:6.2")
.withExposedPorts(6379);
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
registry.add("spring.redis.host", redis::getHost);
registry.add("spring.redis.port", redis::getFirstMappedPort);
}
@Autowired
private QuestionService questionService;
@Autowired
private QuestionRepository questionRepository;
@Test
void shouldCreateQuestionSuccessfully() {
// Given
CreateQuestionRequest request = CreateQuestionRequest.builder()
.title("两数之和")
.description("给定一个整数数组...")
.difficulty("MEDIUM")
.tags(List.of("数组", "哈希表"))
.build();
// When
Question created = questionService.createQuestion(request);
// Then
assertThat(created).isNotNull();
assertThat(created.getId()).isNotNull();
assertThat(created.getTitle()).isEqualTo("两数之和");
// 验证数据库
Optional<Question> saved = questionRepository.findById(created.getId());
assertThat(saved).isPresent();
}
}
// 性能测试示例
@SpringBootTest
@ActiveProfiles("perf")
class QuestionServicePerformanceTest {
@Autowired
private QuestionService questionService;
@Test
void shouldHandleConcurrentRequests() {
// Given
int concurrentUsers = 100;
int requestsPerUser = 10;
// When
long startTime = System.currentTimeMillis();
CompletableFuture<?>[] futures = new CompletableFuture[concurrentUsers];
for (int i = 0; i < concurrentUsers; i++) {
futures[i] = CompletableFuture.runAsync(() -> {
for (int j = 0; j < requestsPerUser; j++) {
questionService.getQuestionById((long) (j % 100 + 1));
}
});
}
CompletableFuture.allOf(futures).join();
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
// Then
assertThat(totalTime).isLessThan(5000); // 5秒内完成
double throughput = (concurrentUsers * requestsPerUser) / (totalTime / 1000.0);
assertThat(throughput).isGreaterThan(100); // 每秒100请求
}
}
总结
智码判官项目展示了现代Web应用的完整开发流程,从需求分析、架构设计、技术选型到具体实现,涵盖了微服务、前后端分离、容器化等主流技术实践。项目不仅提供了在线判题的核心功能,还具备了良好的扩展性、可维护性和性能表现。
这个项目为企业级应用开发提供了完整的参考实现,具有很高的学习价值和技术参考价值。无论是对于个人开发者学习现代Web开发技术,还是对于企业构建类似的在线教育平台,都具有重要的指导意义。
技术栈的深度整合 + 业务场景的完整覆盖 + 企业级的最佳实践 = 一个值得学习和参考的优秀项目案例