尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于SSM与MySQL的运动场地预约系统:表结构设计与并发防冲突实战

基于SSM与MySQL的运动场地预约系统:表结构设计与并发防冲突实战 简介这是一套基于SSMSpringSpringMVCMyBatis与MySQL的大学运动场地管理系统完整项目面向毕业设计、课程设计及Java Web进阶学习者覆盖个人中心、用户管理、场地信息管理、场地预约管理、器材信息与借用管理、系统简介管理、器材分类管理等模块可清晰展示SSM整合开发与预约类业务的完整流程。压缩包共1302个文件大小约30.37MB以Java源码、JSP页面、JS脚本、CSS样式及SQL数据库脚本为主含128个Java、117个JSP、364个JS、146个CSS同时附带设计文档、部署说明与视频演示目录结构清晰便于快速部署与二次开发。系统采用模块化设计具备场地管理、预约审核、日程安排、统计报表、权限管理等功能技术栈稳定界面直观友好。当前已有96人学习下载适合需要参考完整项目文档完成毕设/课设或希望学习SSM整合流程、场地预约类业务开发的读者。1. 场地预约系统的需求比你想的复杂大学运动场地管理系统的核心不是 CRUD而是“时间片段是不是被别人占住了”。同一块羽毛球场地周一晚上 18:00-20:00 被一个学院订走20:00-22:00 又被另一个社团订走两笔订单互不冲突但如果你只在订单表里存“预约日期”和“场地编号”并发下两单就全乱套。这也是为什么基于 SSM 来做这类系统依然有现实意义Spring MVC 管请求分发和事务边界MyBatis 把“带时间条件的防冲突 SQL”写到你可以精确控制MySQL 侧负责唯一索引和行锁兜底。很多人把该项目当成一个普通的增删改查做完发现并发一压就出双订单问题出在领域建模而不是框架。这篇从表结构设计开始逐步走到 SSM 的代码落地和 MySQL 参数设置适合正在做毕设或接手类似预约系统的开发同学。2. 先建模场地、场次与订单的关系必须落在 MySQL 表结构里2.1 业务对象拆解场地、场次、订单谁先定义大学运动场地管理系统常见的实体有用户学生/教师/管理员、场地体育馆、篮球场、羽毛球场、场次某天某个时间段的某个场地、订单谁预约了哪个场次。多数初稿会漏掉“场次”这个中间概念直接在订单表里存场地 ID、日期、开始时间、结束时间然后靠程序判断冲突。这个设计不是说不能用而是后续很难加规则。我一般会建议拆成三个核心表和两张关联表角色权限表按需再加。场地表保存物理属性比如场地编码、名称、类型、位置场次表把“时间”从订单里抽出来每个场次是一个可被预约的资源单元订单表只记录用户、场次、状态、创建时间。这样设计的直接好处是“取消某一场次”只改一条记录不会影响其他时间段的字段。场次表的主键设计要特别注意不要用自增 ID 直接作为唯一约束的全部。真实场景里同一块场地、同一天、同一个时间段只能存在一个场次这个唯一性必须在表结构层面体现而不是靠 Service 层的 if 判断。2.2 时间段精度以半小时为单位还是一天一条记录运动场地预约的最小单位常见是半小时或一小时。以半小时为单位一个场地一天最多 48 个场次一个月 1440 条一个学校几十块场地一年也才几十万条MySQL 完全扛得住。把时间段作为“场次编号的组成部分”而不是“日期起止时间的两个字段”后续查订单冲突会简单很多。我常用的做法是场次表里存 date、start_time、end_time、venue_id 四个业务字段再加一个 ukvenue_id, date, start_time唯一索引。同一场地同一天同一开始时间只能有一个场次这就从根上消除了“重复生成场次”的可能。如果你需要支持动态时段比如节假日延长时间段可以把时段模板单独做成字典表场次生成时去字典表查询。但注意不要让场次表承担模板管理的职责否则后面统计利用率时 SQL 会写得非常痛苦。2.3 表结构 DDLorder 表名的坑与核心建表语句订单表的表名不要用 orderorder 是 MySQL 的保留字不加反引号直接报语法错误。统一用 booking 或 reservation 更稳妥。下面是订场模块的核心 DDL去掉了日志和审计字段保留业务主链路。CREATE TABLE venue ( id BIGINT PRIMARY KEY AUTO_INCREMENT, venue_code VARCHAR(32) NOT NULL COMMENT 场地编码, venue_name VARCHAR(64) NOT NULL COMMENT 场地名称, venue_type TINYINT NOT NULL DEFAULT 1 COMMENT 1羽毛球 2篮球 3乒乓球, location VARCHAR(128) DEFAULT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1可预约 0停用, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_venue_code (venue_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT场地表; CREATE TABLE session ( id BIGINT PRIMARY KEY AUTO_INCREMENT, venue_id BIGINT NOT NULL, book_date DATE NOT NULL COMMENT 预约日期, start_time TIME NOT NULL, end_time TIME NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1可约 2已约 3锁定, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, UNIQUE KEY uk_venue_session (venue_id, book_date, start_time), KEY idx_book_date (book_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT场次表; CREATE TABLE booking ( id BIGINT PRIMARY KEY AUTO_INCREMENT, booking_no VARCHAR(32) NOT NULL COMMENT 订单号, user_id BIGINT NOT NULL, session_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1已支付 2已取消, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_booking_no (booking_no), UNIQUE KEY uk_user_session (user_id, session_id), KEY idx_session_status (session_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约订单表;注意 booking 表里加了一个 uk_user_session同一用户同一场次只能有一条订单。这个唯一索引很多人会漏掉漏掉的后果就是用户可以重复提交两笔一样的订单最后只能靠代码去重。索引本身没有成本一张千万级的表也就多几个字节每条记录但它在并发下能挡住重复下单。3. SSM 三层实现从 Spring MVC 控制器到 MyBatis 的防冲突 SQL3.1 SSM 的定位为什么合适SSM 是 Spring Spring MVC MyBatis 的组合。这里的“基于 SSM”不是说它有某种不可替代的性能优势而是它的分层足够清晰。Spring 管 Bean 和事务Spring MVC 管路由和参数绑定MyBatis 管 SQL。对于场地预约这类以 SQL 为中心的系统MyBatis 比 JPA 更容易把防冲突逻辑写到明处。配置上需要注意版本兼容Spring 5.x 配 MyBatis 3.5.x 是比较常见的组合。数据库连接池我习惯用 Druid但如果你不想引额外依赖直接用 MyBatis 自带的池子也行。下面给出 spring-datasource.xml 里的关键配置省略了常量部分。bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/sport_venue?useSSLfalseamp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword valueyour_password/ property nameinitialSize value5/ property namemaxActive value20/ property namemaxWait value60000/ /beanurl 里面必须带上 serverTimezoneMySQL 8.x 的驱动对时区敏感不配会直接报异常。maxWait 单位是毫秒设成 60000 意思是如果连接池被占满等待 60 秒后抛异常。生产环境不建议把这个值调太小否则高峰期预约会大面积报错。3.2 Controller 层参数校验放在入口处Controller 层的核心职责是接收请求、转换参数、调用 Service不要在里面写业务判断。下面是一个提交预约请求的接口写法。Controller RequestMapping(/booking) public class BookingController { Resource private BookingService bookingService; PostMapping(/create) ResponseBody public Result create(RequestBody BookingCreateDTO dto) { if (dto.getUserId() null) { return Result.fail(用户ID不能为空); } if (dto.getSessionId() null) { return Result.fail(场次ID不能为空); } boolean success bookingService.createBooking(dto.getUserId(), dto.getSessionId()); return success ? Result.ok() : Result.fail(该场次已被预约); } }这里把 userId 和 sessionId 的判空放在 Controller 层只做最基本的参数校验。真正判断“这个场次还能不能订”必须在 Service 层结合数据库状态来做Controller 层就算查了也没用因为并发下两次请求可能同时通过校验。3.3 Service 层事务先更新场次状态再插入订单预约的核心 Service 方法分两步把场次从“可约”改成“已约”然后向订单表插入记录。这两步必须在同一个事务里任何一个失败都要整体回滚。下面是代码模板。Service public class BookingServiceImpl implements BookingService { Resource private SessionMapper sessionMapper; Resource private BookingMapper bookingMapper; Override Transactional(rollbackFor Exception.class) public boolean createBooking(Long userId, Long sessionId) { int updated sessionMapper.lockSession(sessionId); if (updated 0) { return false; } Booking booking new Booking(); booking.setBookingNo(generateBookingNo()); booking.setUserId(userId); booking.setSessionId(sessionId); booking.setStatus(1); int inserted bookingMapper.insert(booking); return inserted 1; } }lockSession 对应的 SQL 是关键不能用 select 查到 status1 再 update而应该一条 update 直接进行条件变更。3.4 MyBatis 的冲突处理 SQLUPDATE 先行防冲突的核心 SQL 是 UPDATE 语句它利用数据库的行锁机制同一时刻只有一个事务能更新同一行。update idlockSession UPDATE session SET status 2, version version 1 WHERE id #{sessionId} AND status 1 /updateupdate 返回的影响行数就是业务判断依据。如果有两个并发请求同时执行这条语句MySQL 会对目标行加锁第一个事务提交后第二个事务的 update 会发现 status 已经不是 1影响行数为 0从而被判定为预约失败。这种写法把并发控制下沉到了数据库代码层不需要加同步锁。如果不想依赖 status 字段可以用 version 乐观锁替换update 条件里加上 version #{expectedVersion}影响行为 0 则重试或失败。两种选一种就好同时用容易把条件写复杂。4. 并发预约的实战处理索引、事务边界与 MySQL 参数4.1 高并发预约的三个破坏点预约系统并发出问题通常集中在三个位置。第一同一场次的重复预约两个人同时点了提交查的时候都是“可约”update 时都成功。第二同一用户的重复提交用户手抖连点两次提交按钮产生了两条订单。第三跨场次的数据不一致先扣了场次状态但订单插入失败事务没回滚干净。第三个问题的根源是事务边界没控制好。Spring 的 Transactional 默认只在抛出 RuntimeException 时回滚如果你在 Service 里 catch 了异常然后返回 false事务不会感知到异常前面做的 update 就永久生效了。4.2 防重复下单数据库唯一索引比查重更可靠防止同一用户对同一场次重复下单最省事的办法是依赖表结构里的 uk_user_session 唯一索引。代码层面先 select 再 insert 必然存在时间窗口两个请求同时查到不存在然后同时插入索引直接挡住一个数据库抛出 DuplicateKeyException。你在 Service 层捕获这个异常转成业务提示即可。try { bookingMapper.insert(booking); } catch (DuplicateKeyException e) { return false; }注意捕获的异常不要去匹配错误码字符串不同 MySQL 版本错误码定义有差异直接捕获 Spring 转换后的 DuplicateKeyException 最稳。4.3 MySQL 隔离级别与行锁用默认的 REPEATABLE READ 够了MySQL 默认隔离级别是 REPEATABLE READ很多开发者担心需要改成 READ COMMITTED但在这个场景里默认级别没有问题。因为上面的防冲突 update 是条件更新InnoDB 在当前读时会锁定命中的索引记录无论隔离级别是什么select ... for update 和 update 都是加锁读防止的是幻读和脏写。真正需要检查的是事务里 SELECT 语句的锁定方式。如果你在事务里先 select 再 update必须给 select 加 for update否则普通 select 是非锁定读两个事务可以同时读到 status1随后 update 仍然只有一个成功多了一次无用的查询开销。更干净的做法是直接 update不要 select。4.4 数据库端参数连接数与事务超时开发环境的连接池配置很随意但部署到服务器后要注意两个参数。一个是 maxActive连接数上限最好与 Tomcat 的最大线程数匹配否则线程等连接连接等线程互相死锁。另一个是 MySQL 侧的 wait_timeout默认 8 小时如果连接池不主动回收空闲连接会被数据库断开应用层还在用已经失效的连接就会抛 CommunicationsException。常见做法是给 Druid 配置 testWhileIdle 为 true并设置 validationQuery 为 select 1。同时检查 MySQL 的 wait_timeout 和 max_connections 配置单次压测时观察 show processlist 里 sleep 连接的数量如果堆积说明连接池回收参数没有生效。5. 部署验证与性能检查清单5.1 在本地用 Docker 快速验证 MySQL 环境很多环境问题其实是 MySQL 版本或字符集不一致导致的。本地验证最省心的方式是用 Docker 起一个 MySQL 8.x 实例测试完直接删掉不影响宿主机环境。docker run -d --name venue-mysql \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEsport_venue \ -p 3306:3306 \ mysql:8.0 \ --character-set-serverutf8mb4 \ --collation-serverutf8mb4_unicode_ci容器启动后先不急着导入表结构等十几秒再执行 docker ps 确认状态。如果你本机 3306 已经被其他 MySQL 占用把 -p 参数改成 3307:3306JDBC 连接串里也要对应改端口。5.2 验证并发冲突的压测方法部署完成后我习惯写一个最简单的并发验证脚本直接用 Jmeter 或 Postman 的 runner 同时发 50 个请求到同一个场次的预约接口。没有压测工具也可以用 bash 的并发模拟。for i in $(seq 1 50); do curl -s -X POST http://localhost:8080/booking/create \ -H Content-Type: application/json \ -d {\userId\:$i,\sessionId\:1} done wait执行完后去数据库查一下当前场次下的有效订单数量期望值是 1 而不是 50。如果查出来多条说明防冲突逻辑没有起效此时优先排查 lockSession 的 SQL 是否真的生效而不是去查 Service 代码。5.3 最后一条检查索引是否被 SQL 真正用上排查慢 SQL 或者防冲突失效第一步就是看执行计划。EXPLAIN SELECT id FROM session WHERE venue_id 1 AND book_date 2025-06-01 AND start_time 18:00;重点看 type 列出现 ref 或 range 说明用到了索引出现 ALL 说明全表扫这种慢查询在高并发下会把数据库拖垮。type 为 ALL 时优先检查字段是否在联合索引的首位venue_id 和 book_date 的顺序不能随意调换MySQL 最左前缀原则决定了查询条件必须命中联合索引的左侧开始部分。如果索引用不上直接就能定位是表结构或查询条件写法出了问题。本文还有配套的精品资源点击获取
返回列表