新闻详情

新闻详情

首页 / 资讯中心 / 详情

Spring Boot + JPA 高并发选课系统实战:防超卖与重复选课

发布时间:2026/9/25 8:43:02来源:尧图网络
Spring Boot + JPA 高并发选课系统实战:防超卖与重复选课
1. 选课系统为什么值得认真做一遍学生选课系统这个题目几乎每个计算机专业的学生都碰过招聘面试里也常被拿来当场景题。但真正把它当回事、按生产标准做一遍的人并不多。大多数人写出来的版本是一张学生表、一张课程表、一张选课记录表Controller 里调一下 ServiceService 里查一下有没有余量有就插入一条记录完事。本地跑起来没问题一放到几百人同时点选课的场面立刻出现超卖、重复选课、数据库连接打满。我前后做过三版选课系统第一版是课程设计交作业的水平第二版给一个学院内部试用第三版才真正把并发问题处理干净。这篇文章就把这三版踩过的坑、改过的方案完整讲一遍从数据建模讲到高并发防超卖。技术栈用的是Spring Boot MySQL JPA这套组合上手快、生态成熟适合作为练手项目也足够撑起一个真实可用的系统。文章适合三类人看正在做课程设计、想拿一个能写进简历的项目练手的同学准备面试、需要把“高并发”从八股文变成能讲清楚实战细节的人以及已经写过选课系统、但被超卖问题卡住的开发者。我会把每一步为什么这么做讲透参数怎么算、锁怎么加、索引怎么建都给出可以直接抄的代码和配置。看完之后你应该能独立复现一个扛得住并发选课的系统并且能说清楚每个设计决策背后的理由。2. 整体架构与数据建模思路2.1 为什么先定架构再写代码很多人一上来就打开 IDE 建 Spring Boot 工程边写边想表结构结果写到一半发现字段不够用回头改表、改实体、改接口返工成本极高。选课系统虽然不大但它涉及的核心矛盾很典型读多写少、写操作有强一致性要求、并发集中在少数热门课程上。这三个特点决定了架构和数据模型必须先想清楚。我的做法是先画一张业务流转图在纸上画就行不用工具学生登录 → 浏览课程列表 → 点击选课 → 系统校验资格与余量 → 扣减余量并生成选课记录 → 返回结果。这条链路里真正需要保护的是“校验余量”和“扣减余量”这两步它们必须是一个原子操作否则并发下必然超卖。想清楚这一点后面的技术选型就顺了。分层上我采用经典的 Controller → Service → Repository 三层但额外加了一层Domain Service专门处理选课这种跨实体的业务逻辑。这样做的原因是选课逻辑既不属于学生实体也不属于课程实体硬塞进任何一个实体里都会让代码变味。分层清晰之后单元测试也好写后面加缓存、加锁都有明确的落点。2.2 数据表设计三张核心表加两张辅助表选课系统的表不多但每张表的字段和索引都有讲究。核心是三张学生表、课程表、选课记录表。另外两张辅助表是学期表和教师表看需求决定要不要。先看课程表这是并发争抢的焦点CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_code VARCHAR(32) NOT NULL COMMENT 课程编号, course_name VARCHAR(128) NOT NULL COMMENT 课程名称, teacher_id BIGINT NOT NULL COMMENT 授课教师, semester VARCHAR(32) NOT NULL COMMENT 学期, capacity INT NOT NULL DEFAULT 0 COMMENT 总容量, selected_count INT NOT NULL DEFAULT 0 COMMENT 已选人数, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, status TINYINT NOT NULL DEFAULT 1 COMMENT 1开放 0关闭, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_code_semester (course_code, semester), KEY idx_semester_status (semester, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个关键决策。第一容量和已选人数放在课程表里而不是每次去 count 选课记录表。count 操作在并发下既慢又容易读到不一致的数据用一个冗余字段selected_count直接维护配合事务保证一致性性能好得多。第二加了version字段用于乐观锁这是防超卖的核心手段之一。第三uk_code_semester唯一索引保证同一学期同一课程编号不重复idx_semester_status支撑按学期查开放课程的常见查询。选课记录表的设计要点在于唯一约束CREATE TABLE course_selection ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, select_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, status TINYINT NOT NULL DEFAULT 1 COMMENT 1有效 0已退选, UNIQUE KEY uk_student_course (student_id, course_id), KEY idx_course_status (course_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_student_course这个唯一索引非常重要它是防止同一学生重复选同一门课的最后一道防线。即使应用层的校验因为并发出现漏洞数据库这一层也会直接拒绝重复插入。我第一版就是漏了这个约束测试时用脚本并发提交同一个学生的选课请求结果插进去好几条重复记录排查了半天才发现是应用层校验有竞态。学生表相对简单但要注意密码字段的存储和学号的唯一性CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(32) NOT NULL COMMENT 学号, name VARCHAR(64) NOT NULL, password VARCHAR(128) NOT NULL COMMENT 加密存储, major VARCHAR(64), grade INT, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_no (student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.3 JPA 实体映射与几个容易踩的坑用 JPA 做 ORM 映射代码量比手写 SQL 少很多但有几个坑必须提前知道。第一个是关联关系的加载策略。选课记录关联学生和课程如果默认用 EAGER 加载查一次选课记录会连带把学生和课程全查出来N1 问题立刻出现。我的做法是全部用FetchType.LAZY需要的时候再用JOIN FETCH显式加载。Entity Table(name course) public class Course { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name course_code, nullable false) private String courseCode; Column(name course_name, nullable false) private String courseName; Column(nullable false) private Integer capacity; Column(name selected_count, nullable false) private Integer selectedCount; Version private Integer version; // getter/setter 省略 }注意Version注解这是 JPA 乐观锁的开关。加上它之后每次更新这条记录JPA 会自动在 where 条件里带上 version并且把 version 加一。如果两个事务同时读到 version5一个先提交把 version 改成 6另一个提交时发现 version 已经不是 5 了就会抛OptimisticLockException。这就是乐观锁防超卖的底层机制。第二个坑是批量操作的性能。选课高峰期可能有大量查询如果每条都单独发 SQL数据库压力很大。JPA 提供了BatchSize和hibernate.jdbc.batch_size配置来优化但对于选课这种单条写入为主的场景更重要的是把查询缓存和连接池配好。2.4 技术选型为什么是 JPA 而不是 MyBatis-Plus网上关于 Spring Data JPA 和 MyBatis-Plus 的对比文章很多我的实际体会是选课系统这种领域模型清晰、以实体为中心的 CRUD 场景JPA 更合适。JPA 的实体映射和乐观锁支持是开箱即用的Version一个注解就搞定MyBatis-Plus 虽然也有乐观锁插件但配置起来多几步。而且 JPA 的方法名派生查询比如findBySemesterAndStatus写起来非常快省掉大量简单 SQL。但 JPA 也有短板复杂查询和批量更新不如 MyBatis 灵活。所以我的方案是混合使用常规 CRUD 走 JPA防超卖的核心扣减操作用原生 SQL 或者Modifying的 JPQL把控制权拿回来。这样既享受了 JPA 的开发效率又在关键路径上保留了精细控制的能力。3. 防超卖的核心机制拆解3.1 超卖到底是怎么发生的先把问题讲清楚。假设一门课容量 50当前已选 49。两个学生 A 和 B 几乎同时点选课。系统处理 A 的请求时读到 selected_count49判断 49 50通过准备写入。就在 A 还没提交事务的瞬间B 的请求也读到了 selected_count49因为 A 还没提交B 读的是旧值同样判断通过。然后两个事务先后提交selected_count 变成 51超卖发生。这个问题的本质是**“读-判断-写”这三步不是原子的**。解决思路无非几种把这三步变成原子操作数据库行锁或原子更新或者让并发的一方失败重试乐观锁或者把并发挡在数据库之外分布式锁、消息队列。选课系统的并发量通常在几百到几千用数据库层面的方案就够了不必上分布式锁那么重。3.2 方案一悲观锁简单但要注意锁粒度悲观锁的思路是“先锁住再操作”。在 JPA 里可以用Lock(LockModeType.PESSIMISTIC_WRITE)实现对应数据库的SELECT ... FOR UPDATE。public interface CourseRepository extends JpaRepositoryCourse, Long { Lock(LockModeType.PESSIMISTIC_WRITE) Query(select c from Course c where c.id :id) OptionalCourse findByIdForUpdate(Param(id) Long id); }在选课服务里这样用Transactional public void selectCourse(Long studentId, Long courseId) { Course course courseRepository.findByIdForUpdate(courseId) .orElseThrow(() - new BizException(课程不存在)); if (course.getSelectedCount() course.getCapacity()) { throw new BizException(课程已满); } // 校验是否已选 if (selectionRepository.existsByStudentIdAndCourseId(studentId, courseId)) { throw new BizException(已选过该课程); } course.setSelectedCount(course.getSelectedCount() 1); courseRepository.save(course); CourseSelection selection new CourseSelection(); selection.setStudentId(studentId); selection.setCourseId(courseId); selectionRepository.save(selection); }悲观锁的优点是逻辑直观一定能防住超卖。但缺点也明显锁的粒度是整行而且持有锁的时间覆盖了整个事务。如果事务里还有其他耗时操作比如远程调用、发消息锁会被长时间占用其他学生选同一门课全部阻塞。所以用悲观锁的铁律是事务里只做必要的数据库操作越快越好。还有一个隐蔽的坑SELECT ... FOR UPDATE在没有命中索引时会锁表而不是锁行。所以findByIdForUpdate必须走主键索引这一点要确认执行计划。我第二版就遇到过因为查询条件没走索引导致锁范围扩大到全表整个选课接口响应时间从几十毫秒飙到几秒。3.3 方案二乐观锁高并发下的更优解乐观锁的思路是“先操作提交时检查有没有冲突”。JPA 的Version已经帮我们实现了这套机制代码写起来更自然Transactional public void selectCourseOptimistic(Long studentId, Long courseId) { Course course courseRepository.findById(courseId) .orElseThrow(() - new BizException(课程不存在)); if (course.getSelectedCount() course.getCapacity()) { throw new BizException(课程已满); } if (selectionRepository.existsByStudentIdAndCourseId(studentId, courseId)) { throw new BizException(已选过该课程); } course.setSelectedCount(course.getSelectedCount() 1); courseRepository.save(course); // 提交时检查 version CourseSelection selection new CourseSelection(); selection.setStudentId(studentId); selection.setCourseId(courseId); selectionRepository.save(selection); }当并发冲突发生时save会抛OptimisticLockException。这时候不能直接把异常抛给用户而是应该捕获异常并重试。重试次数一般设 3 次超过就返回“系统繁忙请重试”。public void selectWithRetry(Long studentId, Long courseId) { int maxRetry 3; for (int i 0; i maxRetry; i) { try { selectCourseOptimistic(studentId, courseId); return; } catch (OptimisticLockException e) { if (i maxRetry - 1) { throw new BizException(选课人数过多请稍后重试); } // 短暂退避后重试 try { Thread.sleep(50L * (i 1)); } catch (InterruptedException ignored) {} } } }乐观锁相比悲观锁的优势在于不阻塞读、锁持有时间短在冲突不激烈的场景下吞吐量更高。选课系统的冲突集中在少数热门课程大部分课程冲突很少乐观锁整体表现更好。但要注意如果某门课被几千人同时抢乐观锁的重试次数会急剧上升反而拖慢系统。这时候需要配合限流或者排队机制。3.4 方案三原子更新 SQL把判断和扣减合成一步前面两种方案都是在应用层做判断还有一种更彻底的做法把判断条件写进 SQL 的 where 里让数据库来保证原子性。public interface CourseRepository extends JpaRepositoryCourse, Long { Modifying Query(update Course c set c.selectedCount c.selectedCount 1 where c.id :id and c.selectedCount c.capacity) int incrementSelectedCount(Param(id) Long id); }调用时判断返回值Transactional public void selectCourseAtomic(Long studentId, Long courseId) { if (selectionRepository.existsByStudentIdAndCourseId(studentId, courseId)) { throw new BizException(已选过该课程); } int updated courseRepository.incrementSelectedCount(courseId); if (updated 0) { throw new BizException(课程已满); } CourseSelection selection new CourseSelection(); selection.setStudentId(studentId); selection.setCourseId(courseId); selectionRepository.save(selection); }这条 update 语句在数据库层面是原子的selectedCount capacity这个条件在更新时求值天然避免了超卖。它的性能最好因为不需要加锁也不需要重试。但有个前提必须保证选课记录的唯一性校验也在同一个事务里并且唯一索引兜底。因为existsByStudentIdAndCourseId这个检查本身有竞态两个并发请求可能都通过检查然后都执行 update最后靠唯一索引拦住其中一个。3.5 三种方案的对比与选型建议方案原理优点缺点适用场景悲观锁SELECT FOR UPDATE逻辑直观一定防住阻塞严重锁粒度大并发低、事务短乐观锁Version 版本号不阻塞读吞吐高冲突多时重试多冲突分散的场景原子更新UPDATE 带条件性能最好无锁逻辑分散需唯一索引兜底高并发抢课我的最终方案是原子更新为主唯一索引兜底配合限流。这是三版迭代下来最稳的组合。原子更新解决了超卖唯一索引解决了重复选课限流解决了突发流量把数据库打垮的问题。4. 完整实操从零搭建选课系统4.1 环境准备与工程初始化先把环境搭好。JDK 用 17 或 21MySQL 用 8.0Maven 3.8 以上。Spring Boot 版本选 3.2.x这个版本对 Java 21 的虚拟线程支持比较好后面可以顺手体验一下。用 Spring Initializr 建工程依赖勾选Spring Web、Spring Data JPA、MySQL Driver、Validation、Lombok。建好之后先改application.ymlspring: datasource: url: jdbc:mysql://localhost:3306/course_selection?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghairewriteBatchedStatementstrue username: root password: your_password hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 3000 max-lifetime: 1800000 jpa: hibernate: ddl-auto: validate show-sql: false properties: hibernate: format_sql: true jdbc: batch_size: 50 order_inserts: true连接池参数不是随便填的。maximum-pool-size设 20 是基于这样的估算假设单次选课事务耗时 20ms那么一个连接每秒能处理 50 个请求20 个连接就是 1000 QPS足够应对大部分校园场景。设太大反而会因为线程上下文切换和数据库连接开销拖慢系统。connection-timeout设 3000ms意思是拿不到连接最多等 3 秒超过就快速失败避免请求堆积。ddl-auto用validate而不是update这是生产环境的习惯。update会自动改表结构看着方便但线上环境自动改表是灾难。表结构用 SQL 脚本手动管理启动时只校验实体和表是否匹配。4.2 核心选课接口的实现Controller 层保持轻薄只做参数校验和结果包装RestController RequestMapping(/api/selection) public class SelectionController { private final SelectionService selectionService; public SelectionController(SelectionService selectionService) { this.selectionService selectionService; } PostMapping(/select) public ResultVoid select(RequestBody Valid SelectRequest request) { selectionService.selectCourse(request.getStudentId(), request.getCourseId()); return Result.success(); } }Service 层是核心把前面讲的原子更新方案落地Service public class SelectionService { private final CourseRepository courseRepository; private final CourseSelectionRepository selectionRepository; public SelectionService(CourseRepository courseRepository, CourseSelectionRepository selectionRepository) { this.courseRepository courseRepository; this.selectionRepository selectionRepository; } Transactional(rollbackFor Exception.class) public void selectCourse(Long studentId, Long courseId) { // 1. 快速失败已选过直接返回 if (selectionRepository.existsByStudentIdAndCourseId(studentId, courseId)) { throw new BizException(您已选过该课程); } // 2. 原子扣减条件不满足返回 0 int updated courseRepository.incrementSelectedCount(courseId); if (updated 0) { throw new BizException(课程已满或不存在); } // 3. 写入选课记录唯一索引兜底 try { CourseSelection selection new CourseSelection(); selection.setStudentId(studentId); selection.setCourseId(courseId); selection.setStatus(1); selectionRepository.save(selection); } catch (DataIntegrityViolationException e) { // 唯一索引冲突说明并发下重复选课回滚扣减 throw new BizException(请勿重复选课); } } }这段代码有几个细节值得说。第一existsByStudentIdAndCourseId这个前置检查是为了减少无谓的数据库写操作它本身不能保证唯一性真正的保证是唯一索引。第二incrementSelectedCount返回 0 有两种可能课程不存在或者已满。为了给用户更准确的提示可以先查一次课程是否存在但那样又多一次查询。我的做法是统一提示“课程已满或不存在”用户体验上可以接受。第三捕获DataIntegrityViolationException后抛业务异常事务会回滚扣减的 selected_count 也会恢复不会出现“扣了名额但没选上”的情况。4.3 退选逻辑与名额回补退选比选课简单但也要注意并发。退选时把选课记录状态改成 0同时把课程已选人数减一Transactional(rollbackFor Exception.class) public void dropCourse(Long studentId, Long courseId) { int updated selectionRepository.updateStatus(studentId, courseId, 0); if (updated 0) { throw new BizException(未找到有效的选课记录); } courseRepository.decrementSelectedCount(courseId); }decrementSelectedCount的 SQL 要加个保护避免减成负数Modifying Query(update Course c set c.selectedCount c.selectedCount - 1 where c.id :id and c.selectedCount 0) int decrementSelectedCount(Param(id) Long id);退选和选课如果同时发生可能出现“退了又选”的竞态但因为都是原子更新最终一致性是能保证的。这里有个经验退选接口一定要做幂等用户连点两次退选第二次应该返回“未找到有效记录”而不是报错前端体验更好。4.4 用虚拟线程提升并发处理能力Java 21 的虚拟线程是个好东西Spring Boot 3.2 开始支持。选课接口是 IO 密集型等数据库用虚拟线程能显著提升吞吐。开启方式很简单spring: threads: virtual: enabled: true开启后Tomcat 的请求处理会使用虚拟线程每个请求一个虚拟线程阻塞在数据库 IO 时不会占用平台线程。实测在同样的硬件上选课接口的 QPS 能提升 30% 到 50%。但要注意虚拟线程不是银弹如果数据库连接池只有 20 个连接虚拟线程再多也得排队等连接。所以虚拟线程要和连接池大小配合调优不能盲目开大。4.5 压测验证用 JMeter 模拟抢课写完代码不压测等于没写。我用 JMeter 模拟 500 个并发用户同时抢一门容量 50 的课验证防超卖是否生效。测试计划这样配线程数 500Ramp-up 时间 1 秒循环 1 次。HTTP 请求指向/api/selection/select参数用 CSV 数据文件提供不同的 studentId。跑完之后查数据库SELECT selected_count, capacity FROM course WHERE id 1; SELECT COUNT(*) FROM course_selection WHERE course_id 1 AND status 1;正确的结果应该是selected_count 50选课记录数也是 50一条不多一条不少。如果 selected_count 大于 50说明防超卖失效如果记录数大于 50说明唯一索引没生效。我第一版压测时 selected_count 跑到了 63就是没用原子更新导致的。压测还要关注响应时间。正常情况下选课接口的 P99 应该在 200ms 以内。如果超过 1 秒说明有锁竞争或者连接池不够需要排查。5. 常见问题与排查技巧实录5.1 超卖问题的排查思路超卖是最常见也最要命的问题。排查时按这个顺序来先确认扣减操作是不是原子的如果用的是“先查再改”的写法基本可以确定是竞态再看事务边界对不对Transactional有没有加在 public 方法上加在 private 方法上不生效这是新手常犯的错最后看数据库隔离级别MySQL 默认的 REPEATABLE READ 在并发下可能出现快照读导致判断用的是旧数据。一个实用的排查技巧是在扣减前后打日志记录当前 selected_count 和线程 ID压测后分析日志能清楚看到哪些请求读到了相同的值。我当初就是靠这个定位到问题的。5.2 重复选课的三种成因重复选课通常有三个原因。第一是唯一索引没建这是最基础的检查SHOW INDEX FROM course_selection确认。第二是唯一索引建了但没生效可能是字段类型不一致或者字符集问题比如 student_id 在两张表里一个是 BIGINT 一个是 VARCHAR索引匹配不上。第三是应用层用了saveOrUpdate之类的逻辑把已存在的记录又更新了一遍这种情况要改成先查后插或者直接用 insert ignore。5.3 数据库连接池打满的处理压测时如果看到HikariPool-1 - Connection is not available, request timed out说明连接池打满了。先别急着调大maximum-pool-size要分析连接被谁占着。常见原因是事务里有慢查询或者远程调用导致连接长时间不释放。用SHOW PROCESSLIST看数据库端的连接状态用 Arthas 的trace命令看哪个方法耗时最长。如果确实是并发量太大可以调大连接池但要同步调大数据库的max_connections否则应用端连接多了数据库端反而拒绝。我的经验是应用连接池总数不要超过数据库max_connections的 70%留点余量给运维和其他应用。5.4 常见问题速查表问题现象可能原因排查方法解决方案超卖扣减非原子压测后查 selected_count改用原子更新 SQL重复选课缺唯一索引SHOW INDEX加 uk_student_course连接超时连接池打满SHOW PROCESSLIST优化慢查询或调大池接口变慢锁竞争看 P99 响应时间缩短事务或改乐观锁事务不回滚异常被吞检查 catch 块抛 RuntimeException乐观锁重试多热点课程看异常日志频率改原子更新或限流5.5 几个我踩过的坑第一个坑是在事务里做远程调用。我第二版在选课成功后调用了一个消息服务发通知结果消息服务偶尔超时导致选课事务被拖长锁一直不释放其他学生全卡住。后来改成用事务同步回调事务提交后再发消息问题解决。第二个坑是**Transactional和synchronized混用**。有同学想用 synchronized 保证并发安全但 synchronized 锁的是 JVM 内的对象多实例部署时完全失效而且和事务的提交时机配合不好容易出现“锁释放了但事务还没提交”的情况。正确做法是把并发控制交给数据库。第三个坑是索引失效导致锁表。前面提过SELECT FOR UPDATE没走索引会锁全表。用EXPLAIN确认执行计划确保 type 是const或eq_ref不要出现ALL。第四个坑是压测数据不真实。一开始我用 100 个学生压测怎么都测不出问题后来加到 500 个才复现超卖。压测的并发数要接近真实峰值否则测了个寂寞。6. 性能优化与扩展方向6.1 缓存课程列表减轻数据库压力选课高峰期学生反复刷新课程列表这个读操作量很大。用 Redis 缓存课程列表能显著减轻数据库压力。缓存策略是课程列表缓存 30 秒选课成功后主动删除对应课程的缓存。30 秒这个值是个权衡太短缓存没意义太长学生看到的名额不准。Cacheable(value courseList, key #semester) public ListCourseVO listCourses(String semester) { return courseRepository.findBySemesterAndStatus(semester, 1) .stream().map(this::toVO).collect(Collectors.toList()); } CacheEvict(value courseList, allEntries true) public void selectCourse(Long studentId, Long courseId) { // 选课逻辑 }注意缓存和数据库的一致性。选课成功后清缓存下次查询会重新加载。这个方案在选课场景下够用因为名额的实时性要求没那么高学生看到缓存里的名额稍微旧一点可以接受真正扣减时以数据库为准。6.2 限流保护数据库突发流量是选课系统的常态开放选课的那一秒可能有几千人同时点。限流是保护数据库的最后一道防线。用 Guava 的 RateLimiter 或者 Sentinel 都行我用的简单方案是基于 Redis 的令牌桶public boolean tryAcquire(String key, int limit, int windowSeconds) { String script local current redis.call(incr, KEYS[1]) if current 1 then redis.call(expire, KEYS[1], ARGV[1]) end if current tonumber(ARGV[2]) then return 0 else return 1 end; Long result redisTemplate.execute( new DefaultRedisScript(script, Long.class), Collections.singletonList(key), String.valueOf(windowSeconds), String.valueOf(limit)); return result ! null result 1; }限流的粒度可以按学生维度防止单个学生刷接口也可以按课程维度防止热门课程被打爆。我两个都做了学生维度限制每秒 5 次课程维度限制每秒 500 次。6.3 后续可以扩展的方向这个系统做完之后还有几个方向可以继续深挖。一是选课排队热门课程用消息队列削峰请求先入队后台按顺序处理前端轮询结果。二是分库分表如果学生规模到几十万选课记录表可以按学期分表。三是多级缓存本地缓存加 Redis进一步降低数据库压力。四是监控告警用 Micrometer 加 Prometheus 监控选课接口的 QPS、响应时间、错误率出问题能第一时间发现。我个人在实际操作中的体会是选课系统这个项目最大的价值不在于功能多复杂而在于它把并发、事务、索引、缓存这些后端核心知识点串成了一条线。把这一条线走通比看十篇八股文都管用。最后再分享一个小技巧压测的时候把数据库的慢查询日志打开long_query_time设成 0.1 秒跑完压测分析慢查询日志往往能发现一些平时注意不到的索引问题。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

地铁BAS PLC开发为何必须用STEP7 V5.5 2026/9/25 9:15:17

地铁BAS PLC开发为何必须用STEP7 V5.5

简介:本资源是一套完整的地铁环境监控系统(BAS)PLC控制程序工程包,面向自动化、轨道交通及工业控制领域的工程师、高校师生与PLC初学者,聚焦西门子S7系列PLC在真实地铁场景中的工程化应用。资源基于STEP7 V5.5开发&…

阅读更多 →
安全养虾:[Windows]Docker部署OpenClaw详细过程记录——TaoToken统一Key接入飞书机器人 2026/9/25 9:15:17

安全养虾:[Windows]Docker部署OpenClaw详细过程记录——TaoToken统一Key接入飞书机器人

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
A股光模块五大龙头量化横评:谁在AI算力浪潮中最受益? 2026/9/25 9:15:11

A股光模块五大龙头量化横评:谁在AI算力浪潮中最受益?

这两年做投研交流,我被问得最多的一个问题是:"AI算力行情走到现在,光模块还能不能看?" 问这话的人,有2023年就在车上的老玩家,也有2025年才反应过来想上车的踏空者。我的回答一直很明确&#xff…

阅读更多 →
【SAP BASIS】Section 5: SAP System Configuration 2026/9/25 9:15:04

【SAP BASIS】Section 5: SAP System Configuration

12. System Parameters【RZ10】默认系统配置再看下一个instance,修改memory,修改密码(点击Parameter)。点击Parameter,就可以修改配置的参数。【SE38】程序RSPARA,查看所有密码可以查看所有的参数&#xff…

阅读更多 →
【SAP BASIS】Section 2: SAP System 2026/9/25 9:15:04

【SAP BASIS】Section 2: SAP System

目录 2. Architecture of SAP NetWeaver AS 3. Logon Groups 4. AS ABAP Processes 5. Transactional Processing 6. Gateway & Web Prcoess 2. Architecture of SAP NetWeaver AS 三层架构:表示层、应用层、服务层 MS消息服务器(仅中央实例&…

阅读更多 →
@vinext/cloudflare 完全指南:为 vinext 接入 Workers KV、CDN 缓存、Response Store 与 Cloudflare Images 2026/9/25 9:14:45

@vinext/cloudflare 完全指南:为 vinext 接入 Workers KV、CDN 缓存、Response Store 与 Cloudflare Images

后端Web框架SSR 【免费下载链接】vinext Vite plugin that reimplements the Next.js API surface — deploy anywhere 项目地址: https://gitcode.com/gh_mirrors/vi/vinext 点击查看 免费下载 vinext/cloudflare 是 vinext(Vite plugin 重新实现 Next…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉