那一刻,服务器炸了
想象一下这个场景:周五晚上八点,选课系统开放。三万名学生同时点击“提交”按钮。屏幕转圈、报错、再转圈……然后一片白屏。教务处老师看着后台日志里疯狂刷新的 NullPointerException 和数据库连接超时错误,心态彻底崩了。
这不是电影情节,这是每年开学季无数高校教务系统的真实写照。
很多学校用的还是十年前的老系统:JSP页面里混杂着Java代码、业务逻辑散落在各个Servlet里、数据库连接没复用、缓存完全靠内存硬扛……这种架构在几百人规模时还能凑合,一旦并发量上来,直接雪崩。
但你知道吗?即使是JSP+Servlet这种“古老”的技术栈,只要设计得当,照样能撑起万级并发的稳定系统。今天我们就来拆解,如何用传统但稳健的JSP+Servlet架构,构建一个真正稳定、高效、可维护的校园教务管理平台。
一、为什么选课系统总是崩?先搞清楚病因
在动手写代码之前,我们必须先理解那些导致系统崩溃的根本原因。这不是为了抱怨,而是为了对症下药。
1.1 并发控制的缺失
选课系统最核心的矛盾是:资源有限,需求无限。
一门课只有100个名额,但可能有500人想选。如果系统没有做好并发控制,就会出现这种离谱的情况:
- 学生A和学生B同时看到还剩2个名额
- 两人同时提交选课请求
- 数据库把名额扣成0,但录入了2条记录
- 结果:实际选了200人,但系统显示只选了198人,或者更糟——超卖了
这种问题在没有锁机制的系统里几乎是必然发生的。
1.2 数据库连接的浪费
很多早期的JSP系统,每个请求都新建一个数据库连接,处理完就关闭。听起来很合理对吧?但问题在于:
- 创建和销毁数据库连接非常耗时(可能几百毫秒)
- 三万人同时选课,意味着同时有三万个连接请求涌向数据库
- 数据库的连接池瞬间被撑爆,新请求直接拒绝
正确的做法是使用连接池,让连接被复用,而不是每次都新建。
1.3 代码结构的混乱
如果你打开一个传统的教务系统JSP文件,可能会看到这样的代码:
<%
// 获取参数
String courseId = request.getParameter("courseId");
// 连接数据库
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/school", "root", "123456");
// 执行查询
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM course WHERE id='" + courseId + "'");
// 处理结果
while(rs.next()){
out.println("<tr>");
out.println("<td>" + rs.getString("name") + "</td>");
out.println("<td>" + rs.getInt("capacity") + "</td>");
out.println("</tr>");
}
// 关闭资源
rs.close();
stmt.close();
conn.close();
%>
这段代码有至少五个问题:
- SQL注入风险:
courseId直接拼接到SQL语句中,攻击者可以轻松注入恶意代码 - 连接每次新建:没有使用连接池
- 资源管理不当:如果中间抛出异常,
close()可能永远不会执行 - 逻辑与表现混杂:业务逻辑写在JSP里,违反了MVC原则
- 硬编码配置:数据库地址、用户名、密码直接写在代码里
这样的系统,不崩才怪。
1.4 没有缓存机制
很多教务系统在查询课程信息时,每次都去数据库查。但课程信息是相对静态的——一学期内不会频繁变化。如果三万人同时查询同一门课的信息,数据库会被三万次查询淹没。
这就是缓存的用武之地。
二、架构设计:MVC模式的正确打开方式
要解决上述问题,首先需要在架构层面做出改变。MVC(Model-View-Controller)不是新概念,但很多学校的教务系统其实并没有真正遵循它。
2.1 什么是真正的MVC
在JSP+Servlet架构中,MVC的分工应该是:
- Model(模型):负责业务逻辑和数据处理,通常是JavaBean或POJO类
- View(视图):负责展示,即JSP页面,只包含HTML和少量的JSTL标签
- Controller(控制器):负责接收请求、调用Model、选择View,即Servlet
关键点:JSP里不应该有任何Java业务逻辑代码,Servlet里不应该有任何HTML代码。
2.2 项目结构规划
一个规范的教务系统项目结构应该长这样:
WebContent/
├── WEB-INF/
│ ├── web.xml # 部署描述符
│ ├── jsp/ # JSP页面(禁止外部直接访问)
│ │ ├── login/
│ │ │ └── login.jsp
│ │ ├── student/
│ │ │ ├── courseList.jsp
│ │ │ ├── courseDetail.jsp
│ │ │ └── myCourses.jsp
│ │ ├── teacher/
│ │ │ └── courseManage.jsp
│ │ └── admin/
│ │ └── dashboard.jsp
│ └── lib/ # 依赖库
├── css/
│ └── style.css
├── js/
│ └── main.js
└── index.jsp
src/
└── com/sapiens/edu/
├── controller/ # Servlet控制器
│ ├── LoginServlet.java
│ ├── CourseServlet.java
│ └── SelectionServlet.java
├── model/ # 数据模型
│ ├── Course.java
│ ├── Student.java
│ ├── CourseSelection.java
│ └── User.java
├── dao/ # 数据访问层
│ ├── BaseDao.java
│ ├── CourseDao.java
│ ├── StudentDao.java
│ └── SelectionDao.java
├── service/ # 业务逻辑层
│ ├── CourseService.java
│ ├── SelectionService.java
│ └── UserService.java
├── util/ # 工具类
│ ├── DBUtil.java # 数据库连接池
│ ├── CacheManager.java # 缓存管理器
│ └── SessionUtil.java # 会话工具
└── filter/ # 过滤器
├── AuthFilter.java # 认证过滤器
└── EncodingFilter.java # 编码过滤器
这个结构清晰地将关注点分离,每个层次各司其职。当你需要修改数据库查询逻辑时,只需改dao层;需要修改展示样式时,只需改jsp层;需要修改业务规则时,只需改service层。
三、数据库层:连接池是稳定的基石
前面提到,数据库连接的问题必须通过连接池来解决。我们使用Apache Commons DBCP或HikariCP来实现。
3.1 数据库连接池配置
首先,创建一个db.properties配置文件:
# 数据库连接配置
db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/school_edu?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF8
db.username=root
db.password=your_password_here
# 连接池配置
db.maxTotal=100 # 最大连接数
db.maxIdle=30 # 最大空闲连接数
db.minIdle=10 # 最小空闲连接数
db.initialSize=20 # 初始化连接数
db.maxWaitMillis=5000 # 最大等待时间(毫秒)
db.validationQuery=SELECT 1
db.testOnBorrow=true # 借用连接时检测有效性
然后,创建连接池工具类DBUtil.java:
package com.sapiens.edu.util;
import org.apache.commons.dbcp2.BasicDataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ResourceBundle;
/**
* 数据库连接池工具类
* 使用单例模式确保全局只有一个连接池实例
*/
public class DBUtil {
private static BasicDataSource dataSource;
private static final String CONFIG_FILE = "db";
// 静态初始化块,确保连接池只创建一次
static {
try {
// 从配置文件读取参数
ResourceBundle rb = ResourceBundle.getBundle(CONFIG_FILE);
dataSource = new BasicDataSource();
dataSource.setDriverClassName(rb.getString("db.driver"));
dataSource.setUrl(rb.getString("db.url"));
dataSource.setUsername(rb.getString("db.username"));
dataSource.setPassword(rb.getString("db.password"));
// 连接池配置
dataSource.setMaxTotal(Integer.parseInt(rb.getString("db.maxTotal")));
dataSource.setMaxIdle(Integer.parseInt(rb.getString("db.maxIdle")));
dataSource.setMinIdle(Integer.parseInt(rb.getString("db.minIdle")));
dataSource.setInitialSize(Integer.parseInt(rb.getString("db.initialSize")));
dataSource.setMaxWaitMillis(Long.parseLong(rb.getString("db.maxWaitMillis")));
dataSource.setValidationQuery(rb.getString("db.validationQuery"));
dataSource.setTestOnBorrow(Boolean.parseBoolean(rb.getString("db.testOnBorrow")));
// 连接泄露检测(开发环境 useful)
dataSource.setRemoveAbandonedOnBorrow(true);
dataSource.setRemoveAbandonedTimeout(300);
dataSource.setLogAbandoned(true);
} catch (Exception e) {
throw new RuntimeException("数据库连接池初始化失败", e);
}
}
/**
* 获取数据库连接
*/
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
/**
* 关闭连接(归还到连接池)
*/
public static void closeConnection(Connection conn) {
if (conn != null) {
try {
conn.close(); // 实际上是归还到连接池,不是真正关闭
} catch (SQLException e) {
e.printStackTrace();
}
}
}
/**
* 获取连接池实例(用于监控)
*/
public static BasicDataSource getDataSource() {
return dataSource;
}
}
3.2 基础DAO模板
为了避免每个DAO都重复写相同的数据库操作代码,我们创建一个泛型基础DAO:
package com.sapiens.edu.dao;
import com.sapiens.edu.util.DBUtil;
import org.apache.commons.dbcp2.BasicDataSource;
import java.sql.*;
import java.util.*;
/**
* 基础DAO类,提供通用的数据库操作方法
* @param <T> 实体类型
*/
public abstract class BaseDao<T> {
/**
* 执行查询,返回单个对象
*/
protected T queryForObject(Connection conn, String sql, RowMapper<T> rowMapper, Object... params) {
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
pstmt = conn.prepareStatement(sql);
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
rs = pstmt.executeQuery();
if (rs.next()) {
return rowMapper.mapRow(rs);
}
} catch (SQLException e) {
throw new RuntimeException("查询失败: " + sql, e);
} finally {
closeResource(rs, pstmt);
}
return null;
}
/**
* 执行查询,返回对象列表
*/
protected List<T> queryForList(Connection conn, String sql, RowMapper<T> rowMapper, Object... params) {
List<T> result = new ArrayList<>();
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
pstmt = conn.prepareStatement(sql);
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
rs = pstmt.executeQuery();
while (rs.next()) {
result.add(rowMapper.mapRow(rs));
}
} catch (SQLException e) {
throw new RuntimeException("查询失败: " + sql, e);
} finally {
closeResource(rs, pstmt);
}
return result;
}
/**
* 执行更新操作(插入、更新、删除)
*/
protected int executeUpdate(Connection conn, String sql, Object... params) {
PreparedStatement pstmt = null;
try {
pstmt = conn.prepareStatement(sql);
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
return pstmt.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException("执行失败: " + sql, e);
} finally {
closeResource(null, pstmt);
}
}
/**
* 执行批量更新操作
*/
protected int[] executeBatch(Connection conn, String sql, List<Object[]> paramsList) {
PreparedStatement pstmt = null;
try {
pstmt = conn.prepareStatement(sql);
for (Object[] params : paramsList) {
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
pstmt.addBatch();
}
return pstmt.executeBatch();
} catch (SQLException e) {
throw new RuntimeException("批量执行失败: " + sql, e);
} finally {
closeResource(null, pstmt);
}
}
/**
* 关闭资源
*/
private void closeResource(ResultSet rs, Statement stmt) {
try {
if (rs != null) rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (stmt != null) stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 行映射器接口,用于将ResultSet映射为对象
*/
public interface RowMapper<T> {
T mapRow(ResultSet rs) throws SQLException;
}
}
这个基础DAO使用了RowMapper接口,让我们可以在查询结果时灵活地映射对象,而不是在DAO内部硬编码映射逻辑。
3.3 课程DAO实现
现在,我们基于基础DAO来实现课程数据访问:
”`java package com.sapiens.edu.dao;
import com.sapiens.edu.model.Course; import com.sapiens.edu.util.DBUtil;
import java.sql.Connection; import java.sql.SQLException; import java.util.List;
/**
课程数据访问对象 */ public class CourseDao extends BaseDao
{ private static final String TABLE_NAME = “course”;
/**
- 根据ID查询课程 */ public Course findById(Integer id) { String sql = “SELECT * FROM ” + TABLE_NAME + “ WHERE id = ?”; return queryForObject(DBUtil.getConnection(), sql, rs -> { Course course = new Course(); course.setId(rs.getInt(“id”)); course.setName(rs.getString(“name”)); course.setTeacher(rs.getString(“teacher”)); course.setCapacity(rs.getInt(“capacity”)); course.setRegistered(rs.getInt(“registered”)); course.setCredits(rs.getInt(“credits”)); course.setDepartment(rs.getString(“department”)); course.setDescription(rs.getString(“description”)); course.setStatus(rs.getInt(“status”)); // 0-未开放 1-开放选课 2-选课结束 return course; }, id); }
/**
- 查询所有课程
*/
public List
findAll() { String sql = “SELECT * FROM ” + TABLE_NAME + “ ORDER BY id DESC”; return queryForList(DBUtil.getConnection(), sql, rs -> { Course course = new Course(); course.setId(rs.getInt(“id”)); course.setName(rs.getString(“name”)); course.setTeacher(rs.getString(“teacher”)); course.setCapacity(rs.getInt(“capacity”)); course.setRegistered(rs.getInt(“registered”)); course.setCredits(rs.getInt(“credits”)); course.setDepartment(rs.getString(“department”)); course.setDescription(rs.getString(“description”)); course.setStatus(rs.getInt(“status”)); return course; }); }
/**
- 根据状态查询课程(用于选课列表)
*/
public List
findByStatus(Integer status) { String sql = “SELECT * FROM ” + TABLE_NAME + “ WHERE status = ? ORDER BY department, name”; return queryForList(DBUtil.getConnection(), sql, rs -> { Course course = new Course(); course.setId(rs.getInt(“id”)); course.setName(rs.getString(“name”)); course.setTeacher(rs.getString(“teacher”)); course.setCapacity(rs.getInt(“capacity”)); course.setRegistered(rs.getInt(“registered”)); course.setCredits(rs.getInt(“credits”)); course.setDepartment(rs.getString(“department”)); course.setDescription(rs.getString(“description”)); course.setStatus(rs.getInt(“status”)); return course; }, status); }
/**
- 更新课程已选人数 */ public boolean updateRegistered(Integer courseId, int newRegistered) { String sql = “UPDATE ” + TABLE_NAME + “ SET registered = ? WHERE id = ? AND registered < ?”; Connection conn = DBUtil.getConnection(); try { // 使用乐观锁,确保并发安全 int rows = executeUpdate(conn, sql, newRegistered, courseId, newRegistered + 1); return rows > 0; } finally { // 注意:这里不关闭连接,因为连接由调用者管理 } }
