ARTICLE DETAIL

资讯详情

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

Java学生选课系统实战:从解压到高并发数据一致性

Java学生选课系统实战:从解压到高并发数据一致性 简介这是一套基于Java技术栈的学生选课系统完整源码面向计算机相关专业学生、Java后端初学者及需要课程设计或毕业设计参考的开发者帮助解决选课流程管理、数据统计与权限控制等实际问题。资源包为zip格式压缩后约21.84MB文件总数与类型明细上游暂未提供从标签与描述可知内容涵盖Java后端、Vue前端及数据库脚本等核心模块可支撑前后端分离项目的完整搭建与二次开发。目前已有164人学习下载具备一定参考热度。系统采用Spring Boot与Vue.js前后端分离架构数据库选用MySQL实现了用户管理、数据可视化、角色权限控制等功能并加入数据加密与防SQL注入等安全措施。读者可据此掌握选课系统的业务建模、接口设计、图表展示与权限校验思路也可在现有代码基础上按需定制功能适合作为课程实践与项目练手的参考方案。1. 从一份「基于 Java 的学生选课系统.zip」说起它到底能解决什么很多计算机专业的学生和刚转 Java 的开发者手里都躺着一个叫「基于 Java 的学生选课系统.zip」的压缩包。它通常出现在课程设计、毕业设计或者 Java 学习路线的实操环节里解压之后是一堆.java文件、几个 JSP 页面、一份 SQL 脚本外加一个 README。问题在于大部分人拿到它之后只会做两件事改改包名交作业或者直接扔进收藏夹吃灰。真正把它跑起来、把选课并发、数据一致性这些坑踩一遍的人少之又少。这个标题背后其实是一套完整的 Java Web 落地链路面向对象建模、JDBC 或 ORM 持久化、事务控制、并发抢课、权限分级。它适合三类人——正在做课程设计需要一份能跑通的参考实现的学生、想用一个小型项目把 Java 基础和数据库操作串起来的自学者、以及准备 Java 面试时想拿一个真实场景讲清楚「怎么保证数据一致性」的求职者。选课系统看着简单但「同一门课被 200 个人同时点选容量只剩 5 个」这个场景足够把乐观锁、悲观锁、事务隔离级别全部拉出来遛一遍。下面我按自己实际复现和改造过的路径把这份压缩包从解压到能扛并发讲清楚。2. 解压之后先别急着改代码环境、依赖与数据库三件事拿到压缩包第一反应不该是打开 IDE 改包名而是先把运行环境对齐。Java Web 项目翻车十次里有六次是环境不对JDK 版本和编译目标对不上、Tomcat 版本和 Servlet 规范对不上、MySQL 驱动和数据库版本对不上。这一章先把这三件事定死再谈代码。2.1 JDK 与编译目标版本的对齐压缩包里的项目大概率是几年前写的pom.xml或 IDE 配置里写的可能是 Java 8。如果你本地装的是 JDK 17直接编译会看到那句经典的报错警告: 源发行版 17 需要目标发行版 17或者反过来提示源版本过低。这不是玄学是编译器的source和target没对齐。先确认本地版本java -version javac -version如果输出是 17 或更高而项目是 Java 8 写的最稳的做法是装一个 JDK 8 专门跑这个项目而不是硬升。硬升会碰到javax包被移除、反射模块化限制等一堆问题。用 Maven 的话在pom.xml里显式锁死properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /propertiessource和target必须一致encoding必须是 UTF-8否则中文课程名和教师名会变成乱码。参数说明source决定用哪个版本的语法检查target决定生成哪个版本的字节码两者不一致时编译器会用较低的那个并给警告但某些语法糖会直接编译失败。2.2 依赖管理与数据库驱动的选择项目如果带pom.xml先执行一次依赖解析看有没有下载失败的mvn clean dependency:resolve常见问题是 MySQL 驱动版本。老项目写的是mysql-connector-java5.x而你的 MySQL 是 8.x连接时会报Unknown system variable query_cache_size或者时区错误。两个办法要么把驱动升到 8.x要么在 JDBC URL 里补参数。我一般直接升驱动改pom.xmldependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency注意 groupId 从mysql变成了com.mysqlartifactId 也变了这是 8.x 之后的命名调整。如果项目用的是老坐标改完记得同步更新代码里的驱动类名从com.mysql.jdbc.Driver换成com.mysql.cj.jdbc.Driver。JDBC URL 建议写成jdbc:mysql://localhost:3306/course_selection?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrueserverTimezone不写会报时区异常allowPublicKeyRetrieval在 MySQL 8 默认加密方式下不写会连不上。这两个参数是血泪经验别省。2.3 建库建表与初始数据导入压缩包里一般有一份.sql文件先看它有没有CREATE DATABASE语句。没有的话手动建CREATE DATABASE course_selection DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE course_selection; SOURCE /path/to/init.sql;utf8mb4而不是utf8因为 MySQL 的utf8是三字节的存不了 emoji 和部分生僻字课程名里如果有特殊符号会插入失败。导入之后检查三张核心表学生表、课程表、选课记录表。选课记录表上应该有(student_id, course_id)的唯一索引这是防止重复选课的第一道防线没有的话自己补ALTER TABLE course_selection ADD UNIQUE KEY uk_student_course (student_id, course_id);这个唯一索引很关键后面讲并发的时候会反复用到。建完索引用SHOW INDEX FROM course_selection;确认一下。3. 把项目跑起来从 Tomcat 部署到第一个选课请求环境对齐之后下一步是让项目真正启动并响应请求。这一步的目标不是「能打开首页」而是「能完整走通一次选课流程」因为只有走通了你才知道哪些环节是脆弱的。3.1 部署方式的选择与启动排错项目如果是传统 Servlet JSP 结构用 Tomcat 部署最直接。把编译产物打成 war 包扔进webapps或者直接在 IDE 里配 Tomcat。启动时重点看日志里有没有这几类错误第一类是ClassNotFoundException通常是依赖没打进WEB-INF/libMaven 项目执行mvn package时会自动处理手动构建的话要自己拷。第二类是NoClassDefFoundError多半是依赖冲突同一个类有两个版本用mvn dependency:tree找出来排除掉。第三类是数据库连接池初始化失败看context.xml或db.properties里的连接串对不对。启动成功后访问首页如果 404检查web.xml里的url-pattern和实际访问路径是否一致如果 500看堆栈第一行通常是空指针或者 SQL 语法错误。3.2 选课核心逻辑的代码走读找到选课的那个 Servlet 或 Controller核心逻辑一般长这样public void selectCourse(int studentId, int courseId) { // 1. 查询课程剩余容量 Course course courseDao.findById(courseId); if (course.getRemaining() 0) { throw new RuntimeException(课程已满); } // 2. 插入选课记录 selectionDao.insert(studentId, courseId); // 3. 扣减剩余容量 courseDao.decreaseRemaining(courseId); }这段代码在单人操作时没问题但它是典型的「检查后执行」竞态。两个线程同时查到remaining 1都通过检查都插入记录都扣减最后容量变成 -1还多了一条重复选课记录。这就是为什么前面要加唯一索引——它至少能挡住重复选课但挡不住超卖。参数说明studentId和courseId是业务主键remaining是课程表的冗余字段用来快速判断容量。冗余字段的好处是查询快坏处是必须和选课记录表保持一致一旦不一致就是数据一致性问题。3.3 用事务把三步操作包起来上面三步必须在一个事务里否则插入成功但扣减失败数据就脏了。改成Transactional public void selectCourse(int studentId, int courseId) { Course course courseDao.findByIdForUpdate(courseId); // 加行锁 if (course.getRemaining() 0) { throw new RuntimeException(课程已满); } selectionDao.insert(studentId, courseId); courseDao.decreaseRemaining(courseId); }findByIdForUpdate对应 SQL 里的SELECT ... FOR UPDATE它会在这一行上加排他锁其他事务想读这一行必须等。这样就把并发串行化了超卖问题解决。代价是吞吐量下降同一门课的选课请求会排队。参数上要注意FOR UPDATE必须在事务里才生效自动提交模式下加了等于没加另外锁的是索引行如果WHERE条件没走索引会升级成表锁整个课程表都被锁住性能直接崩。4. 并发选课的三种方案对比悲观锁、乐观锁与 Redis 预扣把项目跑通只是起点真正体现水平的是怎么处理高并发选课。这一章把三种主流方案摆出来说清楚各自适用场景和参数怎么调这也是 Java 面试里「怎么保证数据一致性」的高频考点。4.1 悲观锁方案与它的吞吐量边界悲观锁就是上面用的SELECT ... FOR UPDATE思路是「先锁住再操作」。优点是实现简单、绝对不超卖缺点是并发能力差。实测在单机 MySQL 上同一门课的选课 TPS 大概在几百到一千之间取决于事务持有时长。优化方向有两个一是缩小锁范围只锁课程行不锁其他二是缩短事务把非数据库操作比如发通知挪到事务外。配置上要确认 InnoDB 引擎和事务隔离级别。SELECT ... FOR UPDATE在REPEATABLE READ下锁的是索引记录在READ COMMITTED下行为略有不同。查看当前隔离级别SELECT transaction_isolation;如果是REPEATABLE-READ配合唯一索引基本够用。但要注意死锁如果两个事务分别锁了不同课程再互相请求对方的锁就会死锁。MySQL 会自动检测并回滚其中一个应用层要捕获DeadlockLoserDataAccessException并重试。4.2 乐观锁方案版本号与重试机制乐观锁的思路是「先操作提交时检查有没有被别人改过」。在课程表加一个version字段ALTER TABLE course ADD COLUMN version INT DEFAULT 0;扣减容量的 SQL 改成UPDATE course SET remaining remaining - 1, version version 1 WHERE id #{courseId} AND remaining 0 AND version #{version};Java 层判断受影响行数如果是 0 说明被别人抢先改了重试或者返回失败int affected courseDao.decreaseWithVersion(courseId, version); if (affected 0) { // 重试或提示用户 throw new RetryableException(选课冲突请重试); }乐观锁的优点是读不加锁、吞吐量高适合冲突不激烈的场景。缺点是冲突多的时候重试次数暴涨反而更慢。参数上要设重试上限一般 3 次超过就返回失败避免请求堆积。remaining 0这个条件不能省它是最后一道防线保证不会扣成负数。4.3 Redis 预扣减与数据库最终一致如果选课量真的很大比如开学第一秒几万人同时抢数据库扛不住常见做法是把库存预扣放到 Redis。流程是课程容量初始化时写入 Redis选课请求先在 Redis 里DECR扣到负数就拒绝扣成功再异步落库。Long remaining redisTemplate.opsForValue().decrement(course:stock: courseId); if (remaining 0) { redisTemplate.opsForValue().increment(course:stock: courseId); // 回补 throw new RuntimeException(课程已满); } // 异步写数据库 mqProducer.send(new SelectionMessage(studentId, courseId));这里的关键是 Redis 的DECR是原子操作天然防超卖。但引入了新问题Redis 扣成功、数据库写失败怎么办所以要有补偿机制比如消息队列重试加对账。参数上要注意 Redis 的持久化配置如果没开 AOF宕机会丢库存数据。这套方案复杂度最高适合真正的高并发场景课程设计级别用悲观锁就够了别过度设计。5. 踩坑记录选课系统从能跑到能用的五个坎这一章是我自己复现和帮别人调这个项目时踩过的坑每条按现象、原因、解决写都是能直接对号入座的。5.1 中文课程名变问号现象数据库里课程名显示正常但页面上全是???。原因数据库连接串没指定字符编码或者表字符集是latin1。解决JDBC URL 加characterEncodingutf8表改成utf8mb4Tomcat 的server.xml里Connector加URIEncodingUTF-8。三处都改缺一处都可能乱码。5.2 选课成功但容量没减现象选课记录插入了但课程剩余容量还是原值。原因插入和扣减不在同一个事务里或者扣减的 SQL 条件写错。解决确认方法上有Transactional确认扣减 SQL 的WHERE条件能匹配到行用SELECT ROW_COUNT()看受影响行数。如果是 0说明条件没命中。5.3 并发下出现重复选课记录现象同一个学生同一门课出现两条记录。原因没有唯一索引或者唯一索引建在了错误的列上。解决加(student_id, course_id)唯一索引应用层捕获DuplicateKeyException并提示「已选过该课程」。注意唯一索引和逻辑删除会冲突如果用了is_deleted标记唯一索引要带上这一列。5.4 事务不生效现象加了Transactional但回滚没起作用。原因常见有三种——方法不是 public、同类内部调用、异常被 catch 没抛出。解决确认方法是 public把事务方法抽到单独的 Service 里catch 之后要么重新抛出RuntimeException要么手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。5.5 连接池耗尽现象压测一会儿就报Could not get JDBC Connection。原因连接泄漏某个分支没关闭连接或者连接池最大连接数设太小。解决用 try-with-resources 管理连接检查连接池配置maxActive一般设 20 到 50maxWait设 3000 毫秒超时快速失败而不是无限等。6. 让选课系统经得起追问压测验证与一个可复用的检查习惯把功能跑通、把并发方案选好之后最后一步是验证它到底扛不扛得住。我一般用 JMeter 或者简单的多线程脚本压测选课接口重点看三个指标有没有超卖、有没有重复记录、响应时间随并发怎么变。写一个最小压测脚本用 Java 的CountDownLatch模拟同时开抢int threads 200; CountDownLatch latch new CountDownLatch(threads); ExecutorService pool Executors.newFixedThreadPool(threads); for (int i 0; i threads; i) { final int studentId i; pool.submit(() - { try { latch.await(); // 等所有线程就绪 selectionService.selectCourse(studentId, 1); // 抢同一门课 } catch (Exception e) { // 记录失败原因 } finally { latch.countDown(); } }); }跑完之后查数据库SELECT COUNT(*) FROM course_selection WHERE course_id 1;应该等于课程容量SELECT remaining FROM course WHERE id 1;应该是 0。如果记录数大于容量说明防超卖失效如果小于容量说明有请求被误杀。两个方向都要查。验证方法上我习惯做一个「对账 SQL」每次压测后跑一遍SELECT c.id, c.capacity, c.remaining, COUNT(s.id) AS actual FROM course c LEFT JOIN course_selection s ON c.id s.course_id GROUP BY c.id HAVING c.remaining ! c.capacity - actual;这条 SQL 能一次性找出所有容量和实际选课数对不上的课程比人工核对快得多。参数说明capacity是总容量remaining是剩余actual是实际选课数三者必须满足remaining capacity - actual不满足就是数据不一致。进阶一点可以把这条对账逻辑做成定时任务每十分钟跑一次发现不一致就告警。这样即使线上出了并发问题也能第一时间发现而不是等用户投诉。我自己的习惯是任何涉及库存、余额、名额的系统上线前必须有一条对账 SQL这是后悔药平时用不上出事时能救命。这套从解压到压测的路径走下来你会发现「基于 Java 的学生选课系统」远不止一份课程设计作业它是一块很好的试金石能把 Java 基础、数据库事务、并发控制、压测验证串成一条线。面试时被问到「怎么保证数据一致性」你可以直接拿这个场景讲比背八股文有说服力得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表