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

资讯详情

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

基于SpringBoot电影票预订系统:选座锁座与订单超时实战

基于SpringBoot电影票预订系统:选座锁座与订单超时实战 简介这是一套面向高校计算机专业学生与Java初学者、用于课程作业或毕业设计的电影票预订系统完整项目资料基于SpringBoot框架开发配套论文、开题报告与答辩PPT可帮助读者快速搭建一个功能闭环的购票平台并完成选题交付。项目采用IDEA2022、JDK1.8、Tomcat8.5环境前端使用Vue、HTML、JS与CSS后端为SpringBoot数据库为MySQL5.7配合Navicat管理。前台涵盖用户注册登录、电影信息列表与详情、在线选座购票、在线支付、按喜好类别的电影推荐、电影评论与论坛等模块后台则提供管理员对用户、影片、推荐、评论、论坛、订单、购票与支付信息的统一管理注册用户亦可维护个人资料、订票、支付与评价记录。资源包共1229个文件以js、html、css、png、gif等前端资源为主另含java源码、class文件、xml配置、sql脚本及pdf、docx文档压缩包约18.36MB目录结构清晰。目前已有59人学习适合需要完整赛题方案、模块化代码参考与论文写作素材的读者。1. 电影票预订系统从选座锁座到订单超时一套 SpringBoot 方案能扛住什么影院售票和普通电商最大的区别在于「座位是唯一库存」。同一场次同一个座位两个人同时点下去谁都不希望出现「付款成功却出票失败」。电影票预订系统要解决的核心就是这件事场次排期、座位图渲染、选座锁座、下单支付、超时释放、出票核销一条链路走完还不能超卖。用 Java SpringBoot 做这套系统是计算机专业毕业设计里最典型也最容易翻车的题目之一——看着简单真做起来锁座并发、订单超时、座位状态同步全是坑。这篇笔记面向正在做「基于 SpringBoot 电影票预订系统设计与实现」的同学也面向想拿它当练手项目的 Java 开发者把技术选型、库表设计、锁座实现、论文与开题怎么搭讲清楚让你能照着复现一套跑得通、讲得清、答辩扛得住追问的系统。2. 技术选型与工程骨架为什么是 SpringBoot MyBatis-Plus Redis2.1 分层结构与依赖清单一套能写进论文、也能真跑起来的电影票系统后端主流做法是 SpringBoot 做 Web 层MyBatis-Plus 做持久层MySQL 存业务数据Redis 扛座位锁和缓存。前端常见两种Thymeleaf 服务端渲染或者 Vue 前后端分离。毕业设计里如果时间紧Thymeleaf 更快出效果如果想让简历好看Vue 前后端分离更合适。下面是我一般会用的 Maven 依赖骨架版本按你本地 JDK 选JDK 8 配 SpringBoot 2.7.xJDK 17 配 3.x别硬凑。!-- pom.xml 关键依赖版本按本地 JDK 对齐 -- dependencies !-- Web 层提供 REST 接口和内置 Tomcat -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 持久层MyBatis-Plus 简化单表 CRUD -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- Redis座位锁 场次缓存 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency !-- MySQL 驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- 参数校验下单接口必用 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency /dependencies依赖说明spring-boot-starter-web负责接口和 JSON 序列化MyBatis-Plus 的BaseMapper能省掉大量单表 SQL但座位这种带并发语义的操作建议手写 SQLRedis 的spring-boot-starter-data-redis默认用 Lettuce 客户端够用。参数校验别省下单接口的场次 ID、座位 ID 列表、用户 ID 都要NotNull否则脏数据进库后排查起来就是黑匣子。2.2 配置文件里必须改的几个参数application.yml里几个参数直接决定系统能不能跑通尤其是连接池和 Redis 序列化。默认的 JDK 序列化在 Redis 里存对象会乱码必须换成 JSON 序列化否则你redis-cli看到的全是二进制调试时想哭。spring: datasource: url: jdbc:mysql://localhost:3306/movie_ticket?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password hikari: maximum-pool-size: 20 # 并发下单时连接池别太小 connection-timeout: 3000 redis: host: localhost port: 6379 database: 0 timeout: 3000 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8参数说明maximum-pool-size设 20 是经验值锁座接口会短暂持有连接太小会在压测时排队超时serverTimezone必须显式指定否则订单时间可能差 8 小时出票时间对不上Redis 的timeout别设太长锁座失败要快速返回而不是让用户干等。RedisTemplate 的序列化配置建议单独写一个Configurationkey 用 StringRedisSerializervalue 用 GenericJackson2JsonRedisSerializer这样存座位锁对象时人能看懂。2.3 库表设计座位状态到底存哪这是整个系统最容易设计错的地方。常见做法有两种一是把每个座位的状态直接存seat表二是只存订单座位状态由订单推导。我推荐前者因为座位图渲染需要快速查状态推导太慢。核心表如下。表名关键字段说明filmid, name, duration, poster影片信息cinema_hallid, name, row_count, col_count影厅行列数决定座位图scheduleid, film_id, hall_id, start_time, price场次座位库存的归属单位seatid, schedule_id, row_no, col_no, status座位status: 0可售 1锁定 2已售orderid, user_id, schedule_id, seat_ids, amount, status, expire_time订单status: 0待支付 1已支付 2已取消order_seatorder_id, seat_id订单与座位关联便于退票释放设计要点seat表按场次冗余一个场次生成一批座位记录schedule_id row_no col_no建唯一索引防止重复生成。order表的expire_time是超时释放的关键下单时写入「当前时间 15 分钟」定时任务扫这个字段。order_seat中间表别省退票时按订单反查座位释放比在seat_ids字符串里解析靠谱得多。3. 选座锁座与订单超时并发场景下怎么不超卖3.1 锁座的两种实现与选型理由锁座本质是「把可售座位改成锁定且只能成功一次」。数据库悲观锁SELECT ... FOR UPDATE能保证但高并发下锁行会拖慢整个场次Redis 分布式锁 Lua 脚本原子操作更快但引入了一致性问题。毕业设计里我一般推荐「Redis 预锁 数据库最终扣减」的组合先用 Redis 的SETNX或 Lua 脚本抢占座位抢到后再异步或同步落库。这样既能在答辩时讲清楚并发控制又不会因为纯数据库锁把系统压垮。// 锁座核心用 Lua 脚本保证「检查 锁定」原子性 // KEYS[1] 场次座位锁前缀, ARGV[1] 座位ID, ARGV[2] 用户ID, ARGV[3] 过期秒数 private static final String LOCK_LUA if redis.call(exists, KEYS[1]) 0 then redis.call(setex, KEYS[1], ARGV[3], ARGV[2]) return 1 else return 0 end; public boolean lockSeat(Long scheduleId, Long seatId, Long userId) { String key seat:lock: scheduleId : seatId; Long result redisTemplate.execute( new DefaultRedisScript(LOCK_LUA, Long.class), Collections.singletonList(key), userId.toString(), 900 // 15 分钟 900 秒 ); return result ! null result 1L; }逻辑说明Lua 脚本在 Redis 单线程里执行exists和setex之间不会被打断这是原子性的来源。ARGV[3]设 900 秒和订单超时时间对齐锁过期后座位自动回到可售避免用户下单不付款把座位永久占死。参数上key 的命名要带scheduleId否则不同场次的同一座位号会互相干扰——这是血泪经验我第一次写就漏了场次维度测试时两个场次抢同一个座位号直接串了。3.2 下单接口的完整流程下单不是简单插一条记录要按顺序做四件事校验场次和座位、逐个锁座、写订单和关联、返回支付信息。任何一步失败都要回滚已锁的座位否则座位会被「幽灵锁定」。Transactional(rollbackFor Exception.class) public OrderVO createOrder(Long userId, Long scheduleId, ListLong seatIds) { // 1. 校验场次存在且未开场 Schedule schedule scheduleMapper.selectById(scheduleId); if (schedule null || schedule.getStartTime().before(new Date())) { throw new BizException(场次不存在或已开场); } // 2. 逐个锁座失败则释放已锁的 ListLong locked new ArrayList(); for (Long seatId : seatIds) { if (!lockSeat(scheduleId, seatId, userId)) { locked.forEach(id - unlockSeat(scheduleId, id, userId)); throw new BizException(座位 seatId 已被占用); } locked.add(seatId); } // 3. 写订单expire_time 当前 15 分钟 Order order new Order(); order.setUserId(userId); order.setScheduleId(scheduleId); order.setAmount(schedule.getPrice().multiply(new BigDecimal(seatIds.size()))); order.setStatus(0); order.setExpireTime(new Date(System.currentTimeMillis() 15 * 60 * 1000)); orderMapper.insert(order); // 4. 写订单座位关联并更新 seat 表状态为锁定 for (Long seatId : seatIds) { orderSeatMapper.insert(new OrderSeat(order.getId(), seatId)); seatMapper.updateStatus(scheduleId, seatId, 1); } return new OrderVO(order.getId(), order.getAmount(), order.getExpireTime()); }参数说明Transactional保证订单和关联表同生共死但注意 Redis 锁不在事务里所以失败时要手动unlockSeat。expire_time用 15 分钟是行业常见值太短用户来不及付款太长座位周转率低。seatMapper.updateStatus建议用带status 0条件的更新返回影响行数为 0 说明座位已被别人改过要抛异常回滚。3.3 订单超时释放的定时任务超时释放是电影票系统的「后悔药」机制。用户下单不付款15 分钟后座位必须自动回到可售否则场次很快就被占满。实现方式有两种Spring 的Scheduled定时扫表或者用延迟队列。毕业设计里定时任务足够简单可控。// 每分钟扫一次过期未支付订单释放座位 Scheduled(cron 0 * * * * ?) public void releaseExpiredOrders() { ListOrder expired orderMapper.selectList( new LambdaQueryWrapperOrder() .eq(Order::getStatus, 0) .lt(Order::getExpireTime, new Date()) ); for (Order order : expired) { // 释放 Redis 锁 ListOrderSeat seats orderSeatMapper.selectByOrderId(order.getId()); for (OrderSeat os : seats) { unlockSeat(order.getScheduleId(), os.getSeatId(), order.getUserId()); seatMapper.updateStatus(order.getScheduleId(), os.getSeatId(), 0); } // 订单置为已取消 order.setStatus(2); orderMapper.updateById(order); } }逻辑说明cron表达式0 * * * * ?表示每分钟第 0 秒执行。查询条件用status 0且expire_time now只处理待支付且已过期的。释放时先删 Redis 锁再改数据库状态顺序反了会出现「数据库可售但 Redis 还锁着」的窗口。注意这个方法要加分布式锁或单机部署多实例部署时同一订单可能被释放两次虽然幂等但浪费资源。4. 避坑与排查锁座、超时、座位图渲染的 5 个真实翻车点4.1 座位被锁死无法释放现象用户下单后没付款15 分钟后座位还是灰色不可选。原因通常是定时任务没生效或者 Redis 锁的过期时间设成了 -1永不过期。排查时先看Scheduled所在类有没有加EnableScheduling这是最常见的低级错误再看 Redis 里ttl seat:lock:*返回值如果是 -1 说明setex写成了set。解决启动类加EnableScheduling锁统一用setex或SET key value EX 900 NX。4.2 同一座位被两个订单同时锁定现象压测时两个请求都返回下单成功order_seat里出现重复座位。原因是锁座和写库之间有间隙或者 Lua 脚本的 key 没带场次维度。解决key 必须seat:lock:{scheduleId}:{seatId}写库时seat表的更新加status 0条件并检查影响行数为 0 就抛异常回滚。数据库层面再给order_seat加(schedule_id, seat_id)唯一索引兜底。4.3 座位图渲染慢或错位现象场次座位图加载要好几秒或者行列对不上。原因是每次渲染都全表查seat或者前端把row_no、col_no当成了数组下标。解决座位图按场次缓存到 Rediskey 用seat:map:{scheduleId}缓存 5 分钟前端渲染时用row_no和col_no显式定位别依赖查询顺序。影厅行列数存在cinema_hall表生成座位时就按它来别在代码里写死。4.4 订单金额和座位数对不上现象选了 3 个座位订单金额只算了 1 个。原因是seatIds传参时前端去重了或者后端用Set接收导致重复座位被合并。解决接口参数用List进方法后先校验size和去重后的size是否一致不一致直接拒绝。金额计算用BigDecimal别用double票价乘数量时浮点误差会让对账出问题。4.5 支付回调后座位状态没更新现象用户付款成功但座位还是锁定状态出票失败。原因是支付回调接口没做幂等或者回调里只改了订单状态没改座位。解决回调接口用订单号做幂等键先查订单状态已支付就直接返回更新座位时把status从 1 改成 2并同步删掉 Redis 锁。回调日志一定要打全出问题时这是唯一的黑匣子。5. 论文、开题与 PPT把能跑的系统讲成能过的答辩5.1 论文框架怎么搭才不空毕业设计的论文最怕写成产品说明书。我一般建议按「绪论 → 相关技术 → 需求分析 → 系统设计 → 系统实现 → 测试 → 总结」走但重点放在设计和实现两章。绪论里把电影票预订的现状和痛点写清楚比如超卖、锁座、超时释放相关技术别堆砌SpringBoot、MyBatis-Plus、Redis 各写一段「为什么选它」就够了。系统设计章放架构图、ER 图、核心表结构实现章放锁座 Lua 脚本、下单流程、定时任务的关键代码和说明。测试章要有并发测试数据比如 100 并发抢同一座位只有 1 个成功这是答辩时最有说服力的证据。5.2 开题报告的任务书怎么写开题任务书的核心是「研究内容」和「技术路线」两块。研究内容按功能模块列影片管理、场次排期、选座锁座、订单支付、超时释放、后台管理。技术路线写清楚前后端分离还是服务端渲染、数据库选型、缓存和锁的方案。进度安排按周排别写太满留两周给联调和论文修改。常见坑是任务书写得太泛比如「实现一个电影票系统」答辩老师一问「你的创新点在哪」就答不上来。把「Redis 原子锁座防超卖」和「定时任务超时释放」写成技术难点比空谈「用户体验」实在得多。5.3 PPT 只讲三件事答辩 PPT 别超过 15 页讲三件事就够系统做了什么功能演示截图、难点怎么解的锁座和超时释放的流程图、测试结果并发数据。演示环节提前录屏现场网络和数据库不一定靠谱。老师最爱问的三个问题提前准备座位怎么防超卖、订单超时怎么释放、Redis 和数据库一致性怎么保证。答案就在第 3 章里背熟锁座 Lua 脚本和定时任务的逻辑基本稳过。5.4 一个能加分的进阶技巧如果时间还够给锁座加一个「排队降级」Redis 抢锁失败时不直接返回失败而是把请求丢进一个本地队列短暂重试 200 毫秒很多「秒杀」场景下用户其实能接受这点延迟。实现上用Thread.sleep加循环重试三次即可别引入 MQ 把系统搞复杂。这个点写进论文的「优化与展望」里答辩时能体现你思考过真实高并发场景比单纯堆功能强。我自己做这类系统最大的教训就是先把锁座和超时这两条链路跑通再堆功能否则功能越多座位状态越乱最后连自己都说不清某个座位到底该是什么状态。希望帮到你。本文还有配套的精品资源点击获取
返回列表