ARTICLE DETAIL

资讯详情

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

Java MVC选课系统实战:JDBC连接池与事务并发控制全解析

Java MVC选课系统实战:JDBC连接池与事务并发控制全解析 简介这是一份基于 Java MVC 架构的学生选课系统项目压缩包面向正在学习 Java Web 开发的初学者以及需要完成课程设计或想快速搭建选课功能原型的人群。系统采用经典的 Model-View-Controller 三层结构前端使用 JSP 动态页面后端包含 Servlet、JavaBean、工具类等覆盖学生登录、课程浏览、选课、退课及后台管理等功能。压缩包为 rar 格式约 320KB共 82 个文件其中 JSP 页面 29 个、Java 源文件 19 个、编译后的 class 文件 19 个另有 XML 配置、数据文件及项目工程文件等目录结构清晰可直接导入 IDE 并部署到 Tomcat 运行。目前已有 190 人学习下载。借助这份资源读者可以对照源码理解 MVC 各层如何协作学习 JSPServletJavaBean 的典型开发方式也能根据实际需求修改功能模块是巩固 Java Web 基础与实战能力的不错参考。1. Java MVC 选课系统为什么每个模块都是“看着简单一做就错”一个 Java MVC 选课系统可能是你下载下来最快、跑起来也最快但真正讲明白却最费劲的课程设计项目。选课系统表面只有登录、查课、选课、退课四个功能本质上是标准增删改查可一旦几十个学生同时点热门课程容量校验失效、事务回滚白做、分页数据错乱这些事会一起冒出来。这篇实战笔记从建表开始把 MVC 分层、JDBC 连接池、选课事务和并发控制完整过一遍最后给一套能现场演示的验证方法适合正在做课程设计、准备毕业答辩或者拿到源码想改成自己项目的读者。你只要有一台能跑 JDK、Tomcat 和 MySQL 的电脑就可以跟着复现。2. 选课系统的数据模型先设计四张表再谈 MVC 分层和 JavaBean拿到压缩包时别急着解压导入 Tomcat第一步先把表结构看明白。选课系统的数据模型很典型学生、课程、选课记录、管理员四张表把多对多关系拆干净MVC 里的 Model 层也顺势有了落点。表结构定了后面写 javabean、dao、servlet 的边界就非常清楚反过来如果一上来就写页面每加一个字段要改三五个文件那种翻车我经历过太多次。2.1 四张核心表的 DDL学生、课程、选课、管理员我用的是 MySQL 5.7建表语句如下。注意四张表全部用 InnoDB因为后面的选课和退课要在事务里更新多行InnoDB 才支持事务和行级锁。CREATE TABLE student ( sno VARCHAR(20) PRIMARY KEY COMMENT 学号主键, sname VARCHAR(50) NOT NULL COMMENT 姓名, spassword VARCHAR(64) NOT NULL COMMENT 密码, sclass VARCHAR(50) COMMENT 班级 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( cno VARCHAR(20) PRIMARY KEY COMMENT 课程编号, cname VARCHAR(100) NOT NULL COMMENT 课程名, teacher VARCHAR(50) COMMENT 任课教师, credit DECIMAL(3,1) COMMENT 学分, capacity INT NOT NULL DEFAULT 60 COMMENT 容量上限, selected INT NOT NULL DEFAULT 0 COMMENT 已选人数冗余计数, begin_time DATETIME COMMENT 选课开始时间, end_time DATETIME COMMENT 选课结束时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE elective ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 自增主键, sno VARCHAR(20) NOT NULL COMMENT 学生号, cno VARCHAR(20) NOT NULL COMMENT 课程号, select_time DATETIME NOT NULL COMMENT 选课时间, UNIQUE KEY uk_sno_cno (sno, cno), CONSTRAINT fk_ele_sno FOREIGN KEY (sno) REFERENCES student(sno), CONSTRAINT fk_ele_cno FOREIGN KEY (cno) REFERENCES course(cno) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT选课关系表; CREATE TABLE admin ( ano VARCHAR(20) PRIMARY KEY, aname VARCHAR(50), apassword VARCHAR(64) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有三处是故意这么设计的。第一学号和课程号都用业务编号当主键而不是自增 int因为选课系统里学号、课号天然唯一用自增反而要额外做唯一索引。第二选课表虽然加了自增 id但真正的防重靠的是uk_sno_cno这个联合唯一键数据库层面拦住同一个人选同一门课两次。第三course 表里的selected是冗余字段正常范式可以左边COUNT(*)统计但选课场景下高频查询剩余容量每次 count 全表代价高用冗余计数配合事务更新更实在。2.2 一个请求在 MVC 里的真实走向很多教程讲 MVC 喜欢画三个圆看得懂但落不了地。选课系统里一次“选课”请求的真实走向是JSP 表单把学号和课号 POST 给 ServletServlet 拿到参数后调用 ServiceService 开启事务再调 DAODAO 把结果映射成 JavaBean 返回Service 提交事务Servlet 再把结果放进 request最后 forward 到 JSP 渲染。谁做什么、哪里容易写错我用表格列出来。层次典型位置选课系统里负责的事最常见的坑Viewwebapp 下的 JSPJSTL 渲染课程列表、回显错误消息在 JSP 里直接写 JDBCControllercontroller 包下的 Servlet收参数、调服务、控制跳转一个 Servlet 堆几百行业务Serviceservice 包事务边界和业务规则校验事务写了但根本没生效DAOdao 包SQL 执行与结果集映射把容量判断写死在 SQL 里Modelmodel 包每张表对应一个 JavaBean字段和表列对不上这个分层不是应付检查的摆设。以“课程列表”为例CourseDao 只负责执行 SQL 返回 ListCourseService 负责算分页总数和校验页码CourseServlet 只做转发逻辑jsp 页面用c:forEach遍历。任何一层出问题日志里看异常堆栈就能定位到具体类而不是在 jsp 里翻 Java 片段。Spring MVC 的分层思想与此同源区别只是把 Servlet 换成了 DispatcherServlet把 DAO 换成了 MyBatis 接口理解这套原始 MVC再看 Spring 相关框架会顺很多。2.3 目录与依赖一个能编译能部署的最小骨架常见课程设计是 Eclipse 加 Maven 的 war 工程目录结构我一般按这个来包名按自己习惯改。src/main/java/com/example/selectcourse/ controller/ LoginServlet.java CourseServlet.java ElectiveServlet.java service/ CourseService.java StudentService.java dao/ StudentDao.java CourseDao.java ElectiveDao.java model/ Student.java Course.java Elective.java Admin.java util/ DBUtil.java src/main/webapp/ WEB-INF/web.xml login.jsp listCourse.jsp courseDetail.jsppom.xml 里最关键的三件事是 servlet-api 用 provided、加上 JSTL、加上数据库驱动。下面这组是在 Tomcat 8.5 加 JDK 1.8 环境下常用的版本组合。dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version3.1.0/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.6/version /dependency /dependencies我特别说下 servlet-api 为什么用 provided。Tomcat 自己带了 servlet-api 实现如果 war 包里再打一份启动时两个同名类互相干扰轻则报警告重则 NoSuchMethodException。所以编译期用它打包期排除它。配置完目录后先跑mvn clean package确认能打出 war 再继续写业务这是最小可运行骨架的验收标准。3. 把登录、查课、选课、退课串起来三段核心 Java 代码与调用链数据模型稳定之后核心流程就是四条线学生登录、课程分页列表、选课、退课。选课系统毕竟是个教学项目页面我用 JSP 服务端渲染不引入前后端分离原因是这个规模下多一层 REST 联调纯属给自己加工作量。真正需要下功夫的是请求链路和事务边界。3.1 登录、Session 写入和过滤器放行规则登录逻辑不复杂但有两个细节直接影响后面所有功能密码校验不能走 SQL 拼接登录成功后必须把用户对象放 Session。先看 LoginServlet 的核心部分。WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String sno req.getParameter(sno); String pwd req.getParameter(pwd); // 参数校验略sno 和 pwd 为空时直接转发回登录页 StudentDao dao new StudentDao(); Student student dao.findBySnoAndPassword(sno, pwd); if (student ! null) { HttpSession session req.getSession(); session.setAttribute(student, student); resp.sendRedirect(req.getContextPath() /listCourse); } else { req.setAttribute(msg, 学号或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }对应 DAO 里的方法是SELECT * FROM student WHERE sno? AND spassword?用 PreparedStatement 绑定参数而不是把 sno 和密码直接拼进 SQL。这一步同时防住了 SQL 注入和中文编码问题后面第 4 章展开细讲。登录写了 Session就必须配一个过滤器统一拦截未登录请求不然每个页面都要自己判断 Session 是否为空。拦截器用WebFilter(/*)拦截所有路径但要放行登录接口和静态资源。WebFilter(/*) public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); // 登录页、登录接口、CSS/JS/图片都放行避免循环重定向 if (uri.endsWith(/login.jsp) || uri.endsWith(/login) || uri.endsWith(.css) || uri.endsWith(.js)) { chain.doFilter(request, response); return; } HttpSession session request.getSession(false); if (session null || session.getAttribute(student) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }注意这里的getSession(false)它不会新建 Session而是只取已有 Session。很多人用getSession()导致未登录用户也被创建了一个空 Session过滤器判断时就失真了。参数上的差别看似小实际影响整个认证逻辑。3.2 课程列表与分页LIMIT、count 和一个不能省的排序字段课程列表页是选课系统的门面学生得先看到有哪些课、还剩多少名额然后才点选课。列表用分页实现每页 6 条Controller 里只处理页码参数。WebServlet(/listCourse) public class CourseServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); int pageNo 1; String pageNoStr req.getParameter(pageNo); if (pageNoStr ! null !pageNoStr.isEmpty()) { pageNo Integer.parseInt(pageNoStr); } int pageSize 6; CourseService service new CourseService(); // 分页查询结果和总页数在 Service 里组装 PageResultCourse page service.findPage(pageNo, pageSize); req.setAttribute(page, page); req.getRequestDispatcher(/listCourse.jsp).forward(req, resp); } }DAO 里的分页 SQL 是核心查询当前页数据用LIMIT ?, ?统计总数另发一条 SQL。SELECT cno, cname, teacher, credit, capacity, selected FROM course ORDER BY selected DESC, cno ASC LIMIT ?, ?; SELECT COUNT(*) FROM course;分页看起来只有两行 SQL但排序字段必须选对。很多人图方便只写ORDER BY selected DESC当多门课已选人数相同时MySQL 返回顺序不固定翻页时就会出现同一行出现在第一页和第二页。课程编号cno是主键加入排序后每行的相对位置就锁死了后面第 5.4 会专门讲这个坑。3.3 选课与退课事务里先查容量再插入判断写在服务层而不是 SQL 里选课是整个系统最值得讲清楚的地方。一个正常的选课流程要做三件事查课程是否存在、查剩余容量是否够、查这个人是否已经选过。这三步必须在一个事务里完成而且 DAO 里不能自己拿到 Connection 去执行否则事务就失效了。public boolean selectCourse(Connection conn, String sno, String cno) throws Exception { // 由 Service 层传入已开启事务的 Connection CourseDao courseDao new CourseDao(); ElectiveDao electiveDao new ElectiveDao(); Course course courseDao.getByNo(conn, cno); if (course null) { throw new BizException(课程不存在); } // 业务规则校验剩余名额 if (course.getSelected() course.getCapacity()) { throw new BizException(该课程已满员); } // 业务规则校验不能重复选 if (electiveDao.exists(conn, sno, cno)) { throw new BizException(你已经选过这门课); } electiveDao.insert(conn, sno, cno); courseDao.increaseSelected(conn, cno); return true; }对应的 CourseDao 里有两个方法很关键。-- 已选人数加一注意 WHERE 条件里带了 selected capacity UPDATE course SET selected selected 1 WHERE cno ? AND selected capacity;这个 UPDATE 的返回值是受影响行数如果等于 0说明课程已经满员Service 层直接抛业务异常。它把容量判断放进了数据库原子操作里防止两个请求同时读到同一个“最后一个名额”。但我要说明白这里的 SQL 条件只是最后一道保险容量校验的业务逻辑仍然写在 Service 层因为异常消息要能区分“课程不存在”“已满员”“已选过”SQL 只知道成功或失败区分不了具体原因。退课的逻辑是反向操作先删选课记录再让已选人数减一。public boolean cancelCourse(Connection conn, String sno, String cno) throws Exception { ElectiveDao electiveDao new ElectiveDao(); CourseDao courseDao new CourseDao(); electiveDao.deleteBySnoAndCno(conn, sno, cno); courseDao.decreaseSelected(conn, cno); return true; }事务、异常处理和连接关闭这层我习惯放在 Service 方法里统一管而不是散在 DAO 中。创作一个假事务的典型写法Service 调conn.setAutoCommit(false)但 DAO 内部每执行一条 SQL 都自己DBUtil.getConnection()等于每个 DAO 方法各开一个事务Service 的回滚什么也管不着。这个问题几乎每个模仿 MVC 的作业里都有排查方法也简单把事务相关代码集中到 ServiceDAO 所有方法都接收 Connection 参数而不是自己取连接。4. JDBC 与连接池参数事务、预编译和连接的 3 个关键选择选了 Servlet 加 JSP 这套老牌 MVC数据库访问层大概率直接用 JDBC。JDBC 本身不难但有一个终极问题怎么管理 Connection。课堂示例代码喜欢每次调用DriverManager.getConnection()用完全部扔掉这在几十人访问的课程设计里勉强能跑一旦有并发请求数据库连接瞬间耗尽。4.1 用 Druid 连接池替换 DriverManager五个初始化参数我的做法是接入阿里巴巴的 Druid 连接池。它对 JDBC 标准接口没有侵入DBUtil 里初始化一个 DataSource之后getConnection()的调用方式不变。public class DBUtil { private static DruidDataSource dataSource; static { dataSource new DruidDataSource(); dataSource.setDriverClassName(com.mysql.jdbc.Driver); dataSource.setUrl(jdbc:mysql://localhost:3306/select_course ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai); dataSource.setUsername(root); dataSource.setPassword(root); dataSource.setInitialSize(5); dataSource.setMaxActive(20); dataSource.setMaxWait(60000); dataSource.setValidationQuery(SELECT 1); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }这里五个参数是血泪经验换来的逐个说。initialSize是启动时预建的连接数设为 5避免第一个请求打进来时才临时建连接。maxActive是最大活跃连接数课程设计 20 足够了调太大反而让 MySQL 默认的连接数吃紧。maxWait是拿不到连接时的最大等待毫秒数设为 60000超过就抛异常防止请求无限阻塞。validationQuery是借用连接前的探活 SQLSELECT 1是 MySQL 最小的健康检查。最容易被忽视的是 URL 里的characterEncodingutf8它保证中文从 JDBC 进入 MySQL 时按 UTF-8 编码少这一行页面里正常的数据进库就会变问号。这个坑我会在第 5 章再展开。有一个概念要特别讲清楚连接池里的close()不是真断开数据库而是把连接归还池子。如果你在finally里忘了 close池里的连接会被借光程序卡在 getConnection 等 60 秒然后报连接超时。所以 DAO 和 Service 的每个方法finally 块都要把 Connection、Statement、ResultSet 逐个关掉顺序是从里往外关。4.2 PreparedStatement 参数绑定写好 SQL 的三个命令JDBC 里执行一条带参数的 SQL正确写法永远是先 prepare 再 bind 再 execute三步缺一不可。String sql SELECT * FROM student WHERE sno ? AND spassword ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, sno); ps.setString(2, pwd); ResultSet rs ps.executeQuery();为什么不用字符串拼接原因有两个。第一是 SQL 注入比如密码框里输 OR 11拼出来的 where 条件恒成立绕过登录验证这在公开的选课系统源码里非常常见。第二是转义问题如果学生姓名里有单引号拼接 SQL 轻则查不出数据重则语法报错。PreparedStatement 的占位符由驱动负责转义这两类问题从根上消失。实际写 DAO 时我一般把 SQL、参数绑定、结果集映射分成三个明确步骤一条查询语句对应一个方法。尤其要注意的是ps.setXxx的类型学号是字符串就 setString课号也是 setString别因为业务上像数字就转成 setInt类型不一致会导致 MySQL 走不到索引数据量大时查询慢得明显。4.3 事务隔离级别与回滚边界选课失败别把无关数据一起回滚事务隔离级别是面试里经常被拷打的问题落到选课系统里反而好理解。MySQL 默认隔离级别是 REPEATABLE_READ但对于选课这个场景我习惯在 Service 里显式降到 READ_COMMITTED。conn.setAutoCommit(false); conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); try { // 业务操作 course courseDao.getByNo(conn, cno); if (course.getSelected() course.getCapacity()) { throw new BizException(已满员); } electiveDao.insert(conn, sno, cno); courseDao.updateSelected(conn, cno); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.close(); }为什么不用 REPEATABLE_READ选课核心是读剩余容量和更新已选人数我们只关心读到的是不是已提交的数据。READ_COMMITTED 读到的都是其他事务已提交的结果在这个场景足够了。换成 Serializable 确实最安全但数据库并发能力断崖式下降一个五六十人的选课系统完全没必要。这里要记住隔离级别越低并发越大但脏读、不可重复读风险越高选什么级别取决于你能不能容忍对应的异常。事务的回滚边界同样重要。回滚的是数据库里这一条选课业务涉及的行而不是 Servlet 里的 request 和 Session。很多新手把conn.rollback()写在了 Servlet 层以为能让页面也恢复原样实际上 web 对象根本不受数据库事务管理。正确做法是事务只在 Service 方法里控制Servlet 捕获业务异常后往 request 里放错误消息再 forward 回列表页让用户看到“该课程已满员”而不是一整页 500 错误。5. 选课系统避坑实录并发超选、中文乱码、分页错乱的 5 个严重坑点选课系统跑通容易稳定很难。下面这五条是我筛过的真实踩坑记录每一条都按“现象、原因、解决”来复盘都是网上代码包里最常见的雷。5.1 热门课程容量瞬间变成负数并发选课缺少原子容量校验现象课程明明只剩 1 个名额最后却选进去 5 个人日志里selected值变成负数页面显示剩余名额 -1。原因Service 层先查selected59判断小于 capacity 后执行 UPDATE 加一。两个请求同时到达都读到 59都通过校验于是两次 UPDATE 把 59 改成 61超卖。解决把容量判断挪进 UPDATE 的 WHERE 条件里变成一条原子 SQL。UPDATE course SET selected selected 1 WHERE cno ? AND selected capacity;执行后判断受影响行数返回 0 就说明这条 update 没有匹配到可更新的行课程已经满员。这种方法不依赖数据库悲观锁也不需要额外等待队列是课程设计阶段性价比最高的修复。另外select_time 字段要按学生实际选课那一刻来填不要在 Servlet 里塞一个固定值避免多个请求共用同一时间。5.2 页面中文全部变成问号三处编码不一致现象登录后学生姓名、课程名在 JSP 页面上显示为“???”或者一片乱码刷新也没用。原因字符集问题几乎都是三处不一致造成的JSP 页面编码、JDBC URL 参数、MySQL 表字段编码只要有一处是 latin1 或默认编码显示就乱。解决JSP 页面头部统一写这两行JDBC URL 按第 4.1 节的写法带上characterEncodingutf8建表时用utf8mb4。已经乱掉的表可以修正数据库默认字符集但旧数据建议手动验证后再保留。ALTER DATABASE select_course CHARACTER SET utf8mb4; ALTER TABLE course CONVERT TO CHARACTER SET utf8mb4;第三个隐蔽点Servlet 里接收 POST 参数前必须设req.setCharacterEncoding(UTF-8)否则中文参数从表单进入 Java 时就已经乱码了后面全白搭。5.3 同一个学生把同一门课选了两遍双重提交压过了业务校验现象选课按钮连点两次配了一套 AJAX 后请求发了两遍查 elective 表出现了两条相同 sno 加 cno 的记录。原因Service 里的exists校验是先用 SELECT 判断再执行 INSERT两次请求都通过判断后插入。业务层校验在这个时间窗口内形同虚设这是典型的并发竞态。解决靠第 2.1 节建表时的uk_sno_cno联合唯一索引兜底。第二次 INSERT 在数据库层直接报 DuplicateKeyExceptionService 捕获后转为业务消息返回给页面而不是让异常一路抛到 500 页面。业务校验能拦下大多数情况唯一索引负责最后一道防线两者不冲突缺一个都不稳。5.4 分页翻到第二页数据还是第一页那几条现象总共有 20 门课每页 6 条点第 2 页后页面刷新内容跟第 1 页几乎一样翻到第 3 页才看到新数据。原因ORDER BY selected DESC排序字段不唯一。当多门课程已选数相同时MySQL 认为这些行顺序等价翻页时行偏移的计算结果就不稳定出现重复或漏行。解决ORDER BY 里补一个主键列。主键唯一排序就稳定。SELECT cno, cname, teacher, credit, capacity, selected FROM course ORDER BY selected DESC, cno ASC LIMIT ?, ?;这是一条很能体现基本功的修复。面试问分页细节时能把这条讲出来比背一堆分页插件 API 有用得多。顺带提一句LIMIT的 offset 要用 int 类型传入不要直接在 SQL 里拼接pageNo字符串否则又回到 SQL 注入那一类问题。5.5 Tomcat 启动失败或页面 404端口、jar 和部署路径三连查现象双击 startup.bat 或从 IDE 启动 Tomcat控制台报端口被占用或者部署后访问http://localhost:8080/xxx/login.jsp直接 404。原因8080 端口被其他进程占了或者 war 包里的 servlet-api 与 Tomcat 自带类冲突部署上下文路径和访问路径不一致。解决按顺序排查。先看端口netstat -ano | findstr :8080有 PID 就杀掉或改 Tomcat 的server.xml里 Connector 端口。把 8080 改成 8081、8082 都行但要记住改完之后所有访问路径跟着变。再看 war 包解压后的目录webapps 下如果是select-course.war访问路径就是http://localhost:8080/select-course/。最后确认 pom.xml 里 servlet-api 是 provided 作用域很多 Tomcat 启动秒崩就是它打进了 lib 目录和容器冲突。6. 现场答辩怎么验证并发修复一个可复现的压测脚本与配套 SQL代码写完了别人问你“你怎么证明并发下不超卖”你不能只说加了事务。我建议准备一个十秒钟能跑完的验证脚本先往 student 表插入 20 个测试学号然后把某门课的 capacity 改成 1、selected 改成 0再模拟 20 个学生同时点选课。for i in $(seq 10 29); do ( curl -s -c /tmp/s$i.txt -d sno2024$ipwd123456 \ http://localhost:8080/select-course/login /dev/null curl -s -b /tmp/s$i.txt -d cnoCS101 \ http://localhost:8080/select-course/elect /dev/null ) done wait这个脚本里每个循环体都被小括号包住并放到后台执行20 个请求近乎同时发出。第一次执行后course 表里如果 selected 大于 1说明容量校验失败加上第 5.1 节的原子 UPDATE 后再跑selected 只会等于 1。脚本跑完用三条 SQL 验收结果。SELECT COUNT(*) FROM elective WHERE cno CS101; SELECT cno, cname, capacity, selected FROM course WHERE cno CS101; SELECT * FROM course WHERE selected capacity;第一条看选课记录条数第二条看课程容量和已选数是否平衡第三条专门捞异常数据。任何一条不合理都说明事务或并发控制还有破绽。做完这个系统我给自己留了个习惯所有涉及写库的接口旁边必须配一条能查出结果的 SQL。这比自信地拍胸口说“没问题”可靠得多也能在答辩现场把“什么时候回滚、怎么防超选、为什么排序要带主键”这些话题稳稳接住。希望帮到你。本文还有配套的精品资源点击获取
返回列表