ARTICLE DETAIL

资讯详情

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

Spring Boot高校实验室预约系统实战:表结构、冲突检测与并发避坑

Spring Boot高校实验室预约系统实战:表结构、冲突检测与并发避坑 简介这是一套基于Spring Boot的高校实验室预约系统完整毕业设计资源面向计算机相关专业正在做毕设的学生以及需要Java项目实战练习的学习者也可用于课程设计与期末大作业。系统围绕用户管理、实验室管理、预约管理等核心模块展开采用前后端分离架构后端整合Spring Web与Spring Data JPA前端使用Vue构建交互界面并实现用户身份验证与授权机制帮助读者理解从数据库设计到接口开发的完整流程。资源包共623个文件约21.93MB其中171个Java源文件构成后端主体59个Vue组件与38个JavaScript文件支撑前端页面另含SQL建库脚本、XML配置、运行说明文档及图片样式等静态资源目录结构清晰便于按模块查阅。已有71人学习下载。读者可获得可直接运行的源码、数据库脚本与部署说明对照调试排错快速掌握Spring Boot应用开发与数据持久化知识。1. 从一张排课冲突表说起高校实验室预约系统到底要解决什么每到学期初实验室管理员最头疼的不是设备不够而是同一台示波器被三个班级同时预约。我见过最离谱的一次某高校电子实验室的预约登记还停留在共享 Excel 阶段结果两个老师带着学生同时站在门口谁都不肯让。这类问题的根子不在设备数量而在预约这件事本身缺少状态管理——谁在什么时间段占了哪台设备系统里没有唯一答案。基于 Spring Boot 的高校实验室预约系统要解决的就是把「人、实验室、时间段、设备」这四个变量锁进一套可校验、可追溯的流程里。它适合两类人一类是正在做课程设计或毕业设计的同学需要一套结构完整、能跑通的后端方案另一类是真的在给学院做信息化改造的开发者关心并发冲突、权限边界和后续维护成本。这篇文章不讲空泛的架构图而是把表结构、冲突检测、状态流转和部署踩坑一条条拆开让你看完能直接动手搭出一版可用的系统。2. 需求拆解与数据建模预约系统最容易翻车的地方2.1 三类角色与核心用例高校实验室预约系统的角色划分比一般预约系统更细常见做法是分成学生、教师、管理员三层。学生只能预约开放时段的公共实验室教师可以预约自己带的课程所需设备管理员负责审批、释放和维护实验室资源。这个划分直接决定了后面权限拦截器的粒度。核心用例其实就五个查空闲时段、提交预约、审批预约、取消预约、生成使用记录。很多教程一上来就画一堆 UML结果代码里连「同一实验室同一时段只能有一条有效预约」都没保证。我一般会先把这五个用例的输入输出写清楚再倒推表结构。用例输入输出关键约束查空闲时段实验室 ID、日期可用时间段列表排除已审批和待审批记录提交预约用户 ID、实验室 ID、起止时间预约单号起止时间不能重叠审批预约预约 ID、审批结果状态变更仅管理员可操作取消预约预约 ID状态变更开始前 2 小时可取消生成使用记录预约 ID使用日志审批通过后自动生成2.2 表结构设计与字段含义数据建模是这类系统的地基。我见过太多项目把预约时间存成两个字符串结果查询时空闲判断全靠 Java 循环数据量一上来就崩。正确做法是用datetime存起止时间并在数据库层加唯一索引兜底。-- 实验室表 CREATE TABLE lab ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT 实验室名称, location VARCHAR(128) COMMENT 位置, capacity INT DEFAULT 0 COMMENT 容纳人数, open_time TIME COMMENT 开放开始时间, close_time TIME COMMENT 开放结束时间, status TINYINT DEFAULT 1 COMMENT 1开放 0关闭 ); -- 预约表 CREATE TABLE reservation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, lab_id BIGINT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待审批 1已通过 2已拒绝 3已取消, purpose VARCHAR(255) COMMENT 使用目的, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_lab_time (lab_id, start_time, end_time) );idx_lab_time这个联合索引是必须的因为冲突检测的查询条件永远围绕lab_id和时间区间展开。status字段用 tinyint 而不是字符串是为了后续做状态机流转时比较方便。注意open_time和close_time存的是实验室的开放窗口预约时间必须落在这个窗口内这个校验放在 Service 层做。2.3 时间冲突检测的 SQL 写法冲突检测是整个系统最核心的一段逻辑。判断两个时间段是否重叠标准条件是new_start existing_end AND new_end existing_start。这个条件覆盖了包含、相交、被包含三种情况。SELECT COUNT(*) FROM reservation WHERE lab_id #{labId} AND status IN (0, 1) AND start_time #{endTime} AND end_time #{startTime};参数说明labId是目标实验室startTime和endTime是本次预约的起止时间。status IN (0, 1)表示只检查待审批和已通过的记录已拒绝和已取消的不参与冲突判断。返回数量大于 0 就说明有冲突直接抛业务异常。提示这段 SQL 必须配合数据库事务使用否则两个请求同时查到 0 再同时插入依然会产生重叠记录。后面第 4 章会讲具体的锁方案。3. 用 Spring Boot 把预约流程跑通从接口到状态机3.1 项目分层与依赖选择一个能维护的 Spring Boot 预约系统分层不需要太花哨Controller、Service、Mapper 三层足够。依赖上我一般只加四样spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、lombok。热搜里常出现的 spring boot mybatis 组合在这里完全够用没必要上 JPA因为预约查询涉及大量自定义 SQLMyBatis 的 XML 映射更直观。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency版本号这里写的是我本地验证过的组合Spring Boot 3.x 需要把 MyBatis starter 换成对应的 3.x 版本否则启动时会报NoClassDefFoundError。如果你用的是 IntelliJ IDEA 社区版创建项目时选 Maven 而不是 Spring Initializr后者在社区版里有时会卡在依赖下载。3.2 预约提交接口的完整实现提交预约的 Service 方法要按顺序做四件事校验实验室是否开放、校验时间是否在开放窗口内、检测冲突、写入记录。顺序不能乱否则会出现「先写库再发现时间非法」的脏数据。Service public class ReservationService { Autowired private ReservationMapper reservationMapper; Autowired private LabMapper labMapper; Transactional(rollbackFor Exception.class) public Long submit(ReservationDTO dto) { Lab lab labMapper.selectById(dto.getLabId()); if (lab null || lab.getStatus() 0) { throw new BizException(实验室不存在或未开放); } // 校验开放窗口 LocalTime start dto.getStartTime().toLocalTime(); LocalTime end dto.getEndTime().toLocalTime(); if (start.isBefore(lab.getOpenTime()) || end.isAfter(lab.getCloseTime())) { throw new BizException(预约时间超出实验室开放时段); } // 冲突检测 int conflict reservationMapper.countConflict( dto.getLabId(), dto.getStartTime(), dto.getEndTime()); if (conflict 0) { throw new BizException(该时段已被预约); } Reservation entity new Reservation(); entity.setUserId(dto.getUserId()); entity.setLabId(dto.getLabId()); entity.setStartTime(dto.getStartTime()); entity.setEndTime(dto.getEndTime()); entity.setStatus(0); reservationMapper.insert(entity); return entity.getId(); } }Transactional注解保证了冲突检测和插入在同一个事务里。BizException是自定义业务异常配合全局异常处理器返回统一格式。注意countConflict的查询和insert之间如果有其他事务提交了重叠记录仍然可能出问题这就是下一章要讲的并发场景。3.3 审批状态流转与权限拦截预约状态从 0 到 1 或 2 的流转只能由管理员触发。我一般用 Spring 的HandlerInterceptor做角色校验而不是在每个 Controller 里写 if-else。public class AdminInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object role request.getSession().getAttribute(role); if (!admin.equals(role)) { response.setStatus(403); response.getWriter().write({\code\:403,\msg\:\无审批权限\}); return false; } return true; } }注册拦截器时只拦截/api/reservation/approve和/api/reservation/reject两个路径不要全局拦截否则学生查空闲时段也会被挡。状态流转本身建议用枚举加 switch 控制拒绝从「已取消」再变回「已通过」这种非法跳转。4. 并发预约与冲突排查那些让你半夜爬起来改代码的坑4.1 避坑五个真实踩过的坑现象一两个请求同时提交数据库里出现两条重叠记录。原因冲突检测的 SELECT 和 INSERT 之间没有锁两个事务都查到 0 条冲突。解决在countConflict的 SQL 后面加FOR UPDATE或者对lab_id做 Redis 分布式锁。单机部署用SELECT ... FOR UPDATE就够了注意要放在事务里才生效。现象二审批通过后学生说看不到自己的预约。原因查询接口只查了status 1但学生提交后状态是 0审批前也应该能看到自己的待审批记录。解决学生端查询条件改成user_id 当前用户 AND status IN (0,1)管理员端才只看 0。现象三取消预约后时段没有释放。原因取消操作只改了status 3但冲突检测的 SQL 里写的是status ! 3把已拒绝的 2 也算进去了。解决统一用status IN (0,1)作为有效预约的判断条件拒绝和取消都不参与冲突。现象四跨天预约时间计算错误。原因LocalTime比较只看了时分秒22:00 到次日 02:00 的预约被判定超出开放窗口。解决跨天场景要么禁止要么把开放窗口判断改成基于完整LocalDateTime的比较别用LocalTime。现象五MySQL 时区不对导致预约时间差 8 小时。原因连接串没配serverTimezoneJDBC 驱动按 UTC 解析。解决连接串加?serverTimezoneAsia/Shanghai同时确认数据库和 JVM 时区一致。4.2 用乐观锁替代悲观锁的取舍FOR UPDATE简单直接但会把行锁住直到事务结束高并发下容易排队。如果实验室数量多、冲突概率低可以用乐观锁在lab表加一个version字段更新时带上版本号。UPDATE lab SET version version 1 WHERE id #{labId} AND version #{version};返回影响行数为 0 就说明有人抢先改了让前端重试。这种方案适合预约分散的场景但如果某个热门实验室被疯抢重试次数会飙升反而不如悲观锁稳定。我的经验是实验室数量少于 20 个就用FOR UPDATE超过 50 个再考虑乐观锁或队列削峰。4.3 接口幂等与重复提交学生手抖连点两次提交按钮会产生两条待审批记录。前端禁用按钮只能防君子后端必须做幂等。最简单的做法是用user_id lab_id start_time做唯一索引插入重复时捕获DuplicateKeyException返回友好提示。ALTER TABLE reservation ADD UNIQUE KEY uk_user_lab_start (user_id, lab_id, start_time);注意这个唯一索引只防同一用户对同一实验室同一开始时间的重复提交不同用户抢同一时段仍然要靠冲突检测。两者是互补关系不是替代关系。5. 部署上线前值得做的三件事压测、监控与数据清理5.1 用 JMeter 压出冲突检测的瓶颈上线前我一定会做一轮并发压测重点不是看 QPS 有多高而是看冲突检测在并发下会不会漏判。用 JMeter 开 50 个线程同时提交同一实验室同一时段的预约正常结果应该是 1 条成功、49 条返回「该时段已被预约」。如果成功数大于 1说明锁没生效回去检查Transactional和FOR UPDATE是否配对。压测时把日志级别调到DEBUG观察 SQL 执行顺序。常见问题是countConflict走了全表扫描因为idx_lab_time没被命中。用EXPLAIN看一下执行计划type列应该是range而不是ALL。5.2 用 Spring Boot Admin 盯住运行时状态热搜里提到的 spring boot admin 在这里很实用。引入spring-boot-admin-starter-server后你可以实时看到预约接口的调用次数、平均耗时和异常率。我一般会重点关注两个指标reservation_submit的 P99 耗时以及冲突检测抛出的BizException数量。后者突然飙升往往意味着有实验室被恶意占位需要人工介入。配置上注意把 Admin Server 和业务服务分开部署别让监控拖慢主服务。如果只是课程设计用 Actuator 的/actuator/health和/actuator/metrics也够用不必强上 Admin。5.3 历史预约数据的归档策略预约表是典型的只增不改的表一个学期下来轻松几十万行。冲突检测的查询虽然走了索引但数据量太大时依然会变慢。我的习惯是每学期末把status IN (2,3)且end_time超过半年的记录迁移到reservation_history表主表只保留有效和近期记录。INSERT INTO reservation_history SELECT * FROM reservation WHERE status IN (2, 3) AND end_time DATE_SUB(NOW(), INTERVAL 6 MONTH); DELETE FROM reservation WHERE status IN (2, 3) AND end_time DATE_SUB(NOW(), INTERVAL 6 MONTH);这两条语句要放在同一个事务里执行并且先备份。归档后记得对主表做一次OPTIMIZE TABLE回收碎片空间。这个操作我一般放在凌晨低峰期用定时任务触发避免手动误删。5.4 一个容易被忽略的细节实验室临时关闭管理员临时关闭某个实验室时如果只改了lab.status 0已经审批通过的预约不会自动失效。正确做法是在关闭实验室的 Service 方法里把该实验室未来所有status 1的预约批量改成status 2并给对应用户发一条站内通知。这个逻辑不复杂但十个人做预约系统有八个会漏掉。Transactional(rollbackFor Exception.class) public void closeLab(Long labId) { labMapper.updateStatus(labId, 0); reservationMapper.rejectFutureByLab(labId); // 通知逻辑省略按项目实际的消息组件实现 }rejectFutureByLab的 SQL 条件是lab_id #{labId} AND status 1 AND start_time NOW()。只处理未来的预约已经开始的不管。这个细节处理好了能省掉大量学生跑到实验室发现门锁了的投诉。我做这类系统最大的教训是别在第一次写的时候就想着支持多校区、多级审批、设备级预约。先把单校区、单级审批、实验室级预约跑通把冲突检测和状态流转做扎实后面加维度只是改查询条件的事。反过来地基没打好功能堆得越多半夜被叫起来修数据的概率就越大。希望帮到你。本文还有配套的精品资源点击获取
返回列表