每到期中期末,教务系统的后台就像早高峰的十字路口。选课按钮一点下去,页面转圈;老师上传成绩单,进度条卡在87%不动;排课规则稍微改一下,服务器CPU直接飙到90%。作为长期跟这类教育信息化项目打交道的人,我太熟悉这种“老树发新枝”的阵痛了。JSP确实是个陪伴了很多学校多年的技术栈,但它天生偏向同步渲染和单体架构,遇到高并发和重型计算就容易“喘不上气”。别急着推倒重来,咱们用“微创手术”的方式,把瓶颈一个个打通,让系统重新平稳跑起来。

选课瞬间的“抢票”效应与前端拦截

选课卡顿的根本原因,往往不是JSP本身有多慢,而是成千上万个请求同时打到数据库,连接池瞬间被占满。传统做法是JSP表单直接提交,浏览器阻塞等待服务器返回整页HTML,这种同步模式在并发面前非常脆弱。

先把请求挡在数据库外面。前端用AJAX替代原生表单提交,配合简单的状态锁,防止学生手抖重复点击。后端用Redis缓存课程余量和学生已选列表,选课校验只在内存里跑一遍,命中缓存才落库。

<!-- 选课按钮区域 -->
<button id="submitSelect" class="btn-primary">确认选课</button>
<div id="loadingTip" style="display:none; color:#d9534f;">正在处理,请稍候...</div>

<script>
document.getElementById('submitSelect').addEventListener('click', function() {
    const btn = this;
    const tip = document.getElementById('loadingTip');
    
    // 防重提交锁
    if (btn.disabled) return;
    btn.disabled = true;
    tip.style.display = 'block';

    fetch('/api/course/select', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({ courseId: 'CS201', semester: '2024-FALL' })
    })
    .then(res => res.json())
    .then(data => {
        if (data.code === 200) {
            alert('选课成功!');
            location.reload();
        } else {
            alert(data.msg || '选课失败,请重试');
        }
    })
    .catch(err => alert('网络异常,请检查连接'))
    .finally(() => {
        btn.disabled = false;
        tip.style.display = 'none';
    });
});
</script>

这段代码看着简单,实际作用很大。它把“等页面刷新”变成了“静默请求”,学生端体验直接丝滑。后台对应的Servlet只需要处理JSON,不再渲染JSP视图,响应时间能从几百毫秒压到几十毫秒。配合Redis的DECR原子操作扣减余量,选课高峰的数据库压力能砍掉大半。

成绩上传的“大文件”堵点与异步解耦

老师上传成绩单慢,通常是因为Excel/CSV解析是CPU密集型操作。很多老系统习惯在JSP页面里直接new HSSFWorkbook(inputStream),文件越大,线程卡得越久,甚至直接触发Tomcat的maxThreads上限。

解决办法很明确:上传即返回,解析往后放。把文件交给消息队列或线程池异步处理,前端用短轮询或WebSocket拿状态。这样即使老师传一份5000行的成绩单,页面也不会卡死。

// 简易异步上传处理(Servlet 3.0+)
@WebServlet("/api/grade/upload")
public class GradeUploadServlet extends HttpServlet {
    private static final BlockingQueue<MultipartFile> queue = new LinkedBlockingQueue<>(100);
    private static final ExecutorService parserPool = new ThreadPoolExecutor(
        4, 8, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(),
        new ThreadPoolExecutor.CallerRunsPolicy()
    );

    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Part filePart = req.getPart("gradeFile");
        String studentId = req.getParameter("studentId");
        
        // 立即返回任务ID,不阻塞当前请求
        String taskId = UUID.randomUUID().toString();
        queue.offer(new MultipartFileWrapper(filePart, studentId, taskId));
        
        resp.setContentType("application/json");
        resp.getWriter().write("{\"code\":200,\"msg\":\"已加入处理队列\",\"taskId\":\"" + taskId + "\"}");
    }
}

// 后台异步解析线程
parserPool.submit(() -> {
    try {
        MultipartFileWrapper item = queue.take();
        // 使用Apache POI流式读取SXSSFWorkbook,避免OOM
        try (InputStream is = item.getFile().getInputStream()) {
            SXSSFWorkbook wb = new SXSSFWorkbook(is, 100); // 100行驻留内存
            Sheet sheet = wb.getSheetAt(0);
            List<GradeEntity> batch = new ArrayList<>();
            
            for (int i = 1; i < sheet.getPhysicalNumberOfRows(); i++) {
                Row row = sheet.getRow(i);
                if (row == null) continue;
                GradeEntity g = new GradeEntity();
                g.setStudentId(item.getStudentId());
                g.setScore(row.getCell(2).getNumericCellValue());
                batch.add(g);
                
                // 每500条批量入库
                if (batch.size() >= 500) {
                    gradeDao.batchInsert(batch);
                    batch.clear();
                }
            }
            if (!batch.isEmpty()) gradeDao.batchInsert(batch);
            
            // 更新任务状态为完成
            statusCache.put(item.getTaskId(), "SUCCESS");
        }
    } catch (Exception e) {
        statusCache.put(item.getTaskId(), "FAILED: " + e.getMessage());
    }
});

这里用了SXSSFWorkbook流式解析和分批入库,内存占用从原来的“吃满整台机器”降到稳定在几百MB。老师点上传后马上就能去忙别的,系统后台悄悄干活,等进度条走完再通知即可。这种“前台放行、后台消化”的模式,特别适合教育场景里那些必须一次性处理大量数据的环节。

排课算法的“死循环”与数据库瘦身

排课卡顿往往藏在两个地方:一是JSP页面里写了复杂的约束判断逻辑,二是数据库查询没有走索引,每次都要全表扫描。排课本质上是个带约束的调度问题,属于NP-Hard,硬算肯定超时。

先把算法从Web层剥离出来。JSP只负责展示结果,排课计算放到定时任务或独立的服务进程里跑。数据库层面,给排课相关的表加上复合索引,比如(teacher_id, course_type, week_range),避免关联查询时产生临时表。

-- 排课表结构优化示例
CREATE TABLE course_schedule (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    teacher_id VARCHAR(32) NOT NULL,
    course_code VARCHAR(20) NOT NULL,
    room_id VARCHAR(20) NOT NULL,
    day_of_week TINYINT NOT NULL, -- 1-7
    period_index TINYINT NOT NULL,
    semester VARCHAR(10) NOT NULL,
    UNIQUE INDEX idx_teacher_room_time (teacher_id, room_id, day_of_week, period_index),
    INDEX idx_semester_course (semester, course_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

JSP查询排课结果时,别再SELECT *了。分页+字段裁剪,配合Ehcache或Redis做短期缓存,能让页面加载时间稳定在200ms以内。

<!-- 排课结果列表页(精简版) -->
<c:if test="${empty scheduleList}">
    <p class="text-muted">暂无排课数据,请稍后刷新或联系教务员。</p>
</c:if>

<table class="table table-bordered">
    <thead>
        <tr><th>课程</th><th>教师</th><th>教室</th><th>时间</th></tr>
    </thead>
    <tbody>
        <c:forEach items="${scheduleList}" var="item">
            <tr>
                <td>${item.courseName}</td>
                <td>${item.teacherName}</td>
                <td>${item.roomNo}</td>
                <td>${item.dayOfWeek}周 第${item.periodIndex}节</td>
            </tr>
        </c:forEach>
    </tbody>
</table>

<!-- 分页控件 -->
<nav>
    <ul class="pagination">
        <c:forEach begin="1" end="${totalPages}" var="p">
            <li class="${p == currentPage ? 'active' : ''}">
                <a href="?page=${p}">${p}</a>
            </li>
        </c:forEach>
    </ul>
</nav>

把“计算”和“展示”拆开,数据库只负责存和查,JSP只负责渲染。这样哪怕排课规则改了十几次,师生端的访问速度也不会跟着波动。

稳定性兜底与日常维护习惯

优化做完,系统跑起来了,但教育场景最怕“一波操作猛如虎,下次高峰又宕机”。真正稳得住的系统,靠的是日常的监控和降级策略。

  1. 连接池监控:用HikariCP替换老旧的DBCP或C3P0,配置maximumPoolSize为CPU核数×2+磁盘IO系数,开启leakDetectionThreshold,连接泄露一眼就能抓出来。
  2. Nginx前置限流:在Tomcat前面加一层Nginx,用limit_req_zone对选课和上传接口做IP级限流,超出阈值的请求直接返回503,保护后端不被拖垮。
  3. 健康检查接口:写一个/health端点,定期返回DB连接数、Redis命中率、线程池活跃数。配合简单的脚本或Prometheus,能提前半小时发现“亚健康”状态。
  4. 灰度发布习惯:JSP页面改版或Servlet逻辑更新,别一次性全量切。先用Nginx权重分流10%流量测试,观察错误日志和响应时间,没问题再逐步放量。

这些动作不需要重写架构,只需要在现有JSP工程里加几行配置、改几个过滤器、调几个参数。很多学校的信息中心老师其实已经具备基础运维能力,按这套思路落地,一周内就能看到明显变化。

写给非技术背景的师生怎么看这套改动

如果你不是写代码的,可能觉得上面一堆ThreadPoolExecutorSXSSFWorkbook有点遥远。换个说法就好理解:选课就像去食堂打饭,以前是每个人自己跑去厨房看有没有菜、能不能做,现在食堂门口装了电子屏(缓存),显示今天剩多少份,大家排队扫码(限流),厨房厨师只负责炒菜(异步解析),饭做好了通知你(状态推送)。排课就像拼乐高说明书,以前每次上课前才现拼,现在提前一天把图纸打印好(定时计算+缓存),上课直接按图搭就行。成绩上传就像交作业,以前是抱着本子站在讲台等老师批改,现在是扔进投递箱,后台慢慢录入,录完发个短信告诉你“已收到”。

技术底层再怎么折腾,最终目的就一个:让老师少盯进度条,让学生少点刷新键,让教务员少接投诉电话。JSP不是不能用,只是得用对地方。把同步变异步,把直连变缓存,把重型计算往后挪,这套组合拳打下来,老系统照样能稳稳托住日常教学。遇到具体报错或性能拐点,随时把日志片段和QPS曲线贴出来,咱们接着往下拆。