1. 系统架构设计与技术选型
1.1 为什么选择SpringBoot+SSM组合
在开发教育类管理系统时,技术栈的选择需要同时考虑开发效率和系统性能。我们最终采用SpringBoot+SSM的组合架构,主要基于以下考量:
- 快速迭代需求:SpringBoot的自动配置特性让开发团队能在1周内完成基础框架搭建,相比传统SSM项目节省约60%的初始化时间
- 教学场景特性:作业评价系统存在明显的学期周期性访问高峰,Spring的IoC容器管理bean生命周期,配合MyBatis二级缓存,实测可承受500+并发查询请求
- 前后端分离实践:虽然系统支持JSP传统模式,但通过预留RESTful接口,后续可无缝切换Vue等前端框架
实际开发中发现,SpringBoot 2.7.3版本与MyBatis 3.5.6存在事务注解兼容性问题,建议使用SpringBoot 2.5.12稳定版
1.2 数据库设计关键点
作业评价系统的数据模型设计有几个特殊考量:
sql复制CREATE TABLE `t_submission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`student_id` varchar(20) NOT NULL COMMENT '学号',
`hw_version` int(11) NOT NULL DEFAULT '1' COMMENT '作业版本号',
`file_path` varchar(255) DEFAULT NULL COMMENT '附件OSS路径',
`submit_time` datetime NOT NULL COMMENT '提交时间',
`auto_score` int(11) DEFAULT NULL COMMENT '自动评分(如查重)',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_student_version` (`student_id`,`hw_version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
- 版本控制设计:通过hw_version字段实现作业多次提交的版本管理
- 文件存储策略:实际测试发现,当单日提交量超过2000份时,直接存数据库会导致性能下降80%,最终采用OSS存储+数据库记录路径的方案
- 索引优化:为student_id和hw_version建立联合唯一索引,使查询速度提升15倍
2. 核心功能实现细节
2.1 多维度评分模块
评价指标体系采用策略模式设计,便于后期扩展新评分维度:
java复制public interface EvaluationStrategy {
EvaluationResult evaluate(Submission submission);
}
@Service
@Qualifier("creativity")
public class CreativityEvaluation implements EvaluationStrategy {
@Override
public EvaluationResult evaluate(Submission submission) {
// 创新性评估算法实现
}
}
实际使用中发现了几个关键问题:
- 教师端评分页面需要动态加载评价维度,我们通过反射获取所有实现EvaluationStrategy的Bean
- 权重配置存储在redis中,修改后5分钟生效,避免频繁读库
- 批量评分时采用异步线程池,100份作业的评分时间从32秒降至8秒
2.2 作业查重算法优化
初始版本使用纯文本余弦相似度算法,存在两个严重问题:
- 对代码类作业识别率不足40%
- 长文本比对消耗CPU资源过高
改进后的方案:
- 对文本作业:SimHash+局部敏感哈希(LSH)
- 对代码作业:AST抽象语法树比对
- 引入缓存机制,重复比对相同作业时直接返回结果
实测显示优化后:
- 查重准确率提升至89%
- CPU占用降低65%
- 平均响应时间从4.3s降至1.2s
3. 性能调优实战记录
3.1 Nginx静态资源缓存配置
前端页面出现加载缓慢问题,通过以下Nginx配置解决:
nginx复制location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
access_log off;
}
关键调整参数:
- 将bootstrap.css等不变资源缓存设置为30天
- 动态API接口设置no-cache
- 启用gzip压缩,资源体积减少70%
3.2 MyBatis二级缓存陷阱
初期直接启用二级缓存导致出现脏读,解决方案:
- 在mapper.xml中精细控制缓存范围:
xml复制<cache eviction="LRU" flushInterval="60000" size="1024" readOnly="true"/>
- 对关键业务方法添加注解:
java复制@CacheEvict(value="submissionCache", key="#studentId")
public void updateSubmission(String studentId) {
//...
}
- 配置监控脚本,当缓存命中率低于60%时自动清理
4. 部署与运维方案
4.1 高可用部署架构
生产环境采用双机热备方案:
- 主从MySQL配置,从库延迟控制在200ms内
- Redis哨兵模式,故障转移时间<3秒
- 应用服务器通过Keepalived实现VIP漂移
压力测试结果:
- 持续1000并发请求下,系统响应时间稳定在800ms以内
- 数据库故障时,服务中断时间控制在15秒内
4.2 监控指标配置
Prometheus监控重点指标:
yaml复制- job_name: 'springboot'
metrics_path: '/actuator/prometheus'
scrape_interval: 15s
alert_rules:
- alert: HighErrorRate
expr: rate(http_server_requests_errors_total[1m]) > 0.1
for: 5m
关键报警阈值设置经验:
- JVM内存使用>80%持续5分钟
- SQL执行时间>2s的请求占比超过5%
- 磁盘IO等待时间>500ms
5. 典型问题排查实录
5.1 批量导出OOM问题
现象:导出1000份作业数据时频繁触发Full GC
排查过程:
- 通过jmap -histo发现大量EvaluationDTO对象未释放
- 检查代码发现分页查询逻辑错误,实际加载了全部数据
- 使用MyBatis的ResultHandler流式处理改进:
java复制sqlSession.select("queryEvaluations", paramMap,
new ResultHandler() {
@Override
public void handleResult(ResultContext context) {
// 逐条处理
}
});
优化后内存占用从2GB降至200MB左右
5.2 定时任务堆积问题
成绩统计任务在期末出现严重延迟:
- 分析日志发现单机处理1000个任务需要4小时
- 改进方案:
- 引入Redis分布式锁
- 按班级拆分子任务
- 增加重试机制(3次指数退避)
java复制@Scheduled(cron = "0 0 2 * * ?")
public void statDailyScores() {
// 获取分布式锁
if(redisLock.tryLock("score_stat", 10, TimeUnit.MINUTES)) {
try {
// 分片处理逻辑
} finally {
redisLock.unlock();
}
}
}
优化后全部任务能在30分钟内完成
6. 安全防护实践
6.1 作业文件安全控制
防止未授权下载的关键措施:
- OSS私有桶+临时URL签名(有效期30分钟)
- 下载前校验用户权限:
java复制if(!submissionService.checkDownloadPermission(userId, fileId)) {
throw new SecurityException("无权限下载");
}
- 日志记录所有下载行为,保留6个月
6.2 SQL注入防御方案
除常规的MyBatis参数化查询外,额外实施:
- 启动时检查所有mapper.xml:
java复制Pattern.compile(".*\\$\\{.*\\}.*").matcher(xmlContent)
- 定期执行SQL注入测试用例
- 对敏感表操作启用审计日志
7. 扩展性设计
7.1 插件化架构设计
通过Spring的@Conditional实现功能模块动态加载:
java复制@Configuration
@ConditionalOnProperty(name = "module.antiCheat.enabled")
public class AntiCheatAutoConfiguration {
@Bean
public SimilarityChecker similarityChecker() {
return new AdvancedSimilarityChecker();
}
}
7.2 多租户改造方案
为支持SaaS化部署,进行的关键改造:
- 数据库增加tenant_id字段
- 通过ThreadLocal传递租户上下文
- 动态数据源路由:
java复制public class TenantDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TenantContext.getCurrentTenant();
}
}
改造后测试显示:
- 新增租户配置时间<10分钟
- 跨租户查询性能损耗<8%