恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

SpringBoot+Vue3影院购票系统实战:并发锁座与订单超时处理

  • 首页
  • 资讯中心
  • /
  • SpringBoot+Vue3影院购票系统实战:并发锁座与订单超时处理

相关资讯

上网第十二课:宽带频繁掉线?装维师傅的5步排查法 2026/10/3 8:41:59
上网第十五课:WiFi 的前世今生:从 802.11 到 WiFi7 2026/10/3 8:41:59
ShuffleNet / ShuffleNetV2:通道混洗驱动,极致低MAC的超轻量CNN终极范式 2026/10/3 8:41:59

最新资讯

智能体工程化浪潮:从框架选型到安全落地的实践观察
Gemini API微调模型403权限错误全解析与排查指南
论文查重与AI检测全解析:2026年免费工具与合规降重指南
基于微信小程序与SSM的医院预约挂号系统设计实践
2026论文降重与降AI双重要求下的免费工具实战指南
Codex与Claude双工具协作:AI编程工作流优化与额度管理实战

今日推荐

SAP生产预留实战指南:MB21/MB23/MB25协同与MRP集成
编译原理实验:递归下降分析器消除左递归与避坑指南
Python协议级爬取Shopee商品数据实战

本周热门

从像素到笔画:srt-whiteboard-animation骨架笔迹追踪实现(Zhang-Suen细化+8邻接追踪)
网站建设的英语怎么说?别只背单词,看完这套安全完整流程才敢上线
新手入门看这篇:建设网站加盟避坑指南与SEO实操

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

SpringBoot+Vue3影院购票系统实战:并发锁座与订单超时处理

发布时间:2026/10/3 8:41:59
SpringBoot+Vue3影院购票系统实战:并发锁座与订单超时处理 很多做毕设或者练手项目的同学一听到“影院购票系统”就以为只是个CRUD增删改查实际动手才发现选座并发、订单超时、前端座位图交互这些才是真正卡人的地方。这套基于Java SpringBoot Vue3 MyBatis MySQL的前后端分离影院购票系统正好把这几个硬骨头都踩了一遍。本文不写教科书而是把我从数据库设计到最后部署遇到的真实问题和解决思路完整盘出来适合正在做Java项目实战、准备面试或需要一套可运行源码参考的人。项目覆盖了用户端购票全流程和后台管理端麻雀虽小五脏俱全。1. 项目整体设计与技术选型1.1 为什么是前后端分离而不是传统的Thymeleaf模板渲染影院购票系统的核心交互在选座上。用户点开一个场次要马上看到整个影厅的座位布局、实时感知已被锁定的座位、付款倒计时这些状态变化如果用服务端模板渲染每次点击都要刷新页面体验非常糟糕。前后端分离之后前端Vue3负责局部更新座位状态后端只提供数据接口公众号、App、小程序未来也能复用同一套API。技术栈选型上SpringBoot 2.7.x搭配MyBatis是Java生态里最稳妥的组合。MyBatis比JPA好在SQL可控购票系统涉及多表联查、动态条件筛选、乐观锁更新SQL手写比自动生成的更清晰高效。Vue3选择配合Vite来做开发服务器和打包比旧式Vue CLI启动快很多而且Composition API写座位图这种复杂状态管理比Options API顺手。前端用Vue3 Element Plus Pinia Vue Router后端用SpringBoot MyBatis MySQL 8.0鉴权用JWT。整套技术路线没有肯德基豪华午餐式的堆砌每个组件都有明确的落地场景。1.2 系统模块拆解从购票到管理的完整闭环整个系统分成两端用户端和管理端。用户端核心是影片浏览、影院排片、选座下单、模拟支付、订单查询管理端负责影片管理、影厅管理、场次排期、订单查看。虽然很多人只关注购票流程但排片管理才是这套系统的灵魂没有排片就没有场次没有场次就没有选座业务链是串在一起的。模块拆分上我按domain来划分包结构user、film、cinema、session、seat、order、pay。每个包里放Controller、Service、Mapper三层。有人喜欢按技术分层拆包controller包、service包但是业务多了以后会乱成一团按业务域拆分更符合DDD的轻量实践后期扩展改起来也快。1.3 数据库核心表设计与关键字段取舍数据库是这套系统的地基我设计了六张核心表用户表、影片表、影厅表、场次表、座位订单表、订单明细表。其中场次表是关键中的关键它要把影片ID、影厅ID、放映时间、票价都关联起来CREATE TABLE tb_session ( id BIGINT PRIMARY KEY AUTO_INCREMENT, film_id BIGINT NOT NULL, hall_id BIGINT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 1 COMMENT 1-售票中 2-已结束 3-已取消, INDEX idx_film_start (film_id, start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;座位数据不单独存一张“影厅座位主档表”而是做成seats字段存JSON记录哪些座位已被锁定。这个设计很多人不理解觉得应该建一张seat表加状态字段。实际上购票系统的座位状态是临时性的一场电影的座位状态只在放映前有意义为每个场次预生成几百条座位记录数据膨胀很快。用一个JSON字段记录已售/已锁坐标配合Redis做缓存简单直接。订单表设计需要注意唯一订单号我用了“日期随机数用户ID尾部”拼接避免业务主键冲突。同时需要加状态字段标识待支付、已支付、已取消状态和锁座超时任务配合工作。2. 核心业务逻辑与难点实现2.1 选座与锁座的并发方案乐观锁 唯一约束兜底选座是并发冲突最严重的环节热门场次开售1000个人同时抢同一批座位如果处理不好就会出现“超卖”两个人买到了同一个座位。我采用的是两段式思路先从Redis里尝试CAS占座写入数据库时再用唯一约束兜底。Redis占座用的是SETNX把seatId拼接场次ID作为keyvalue存用户ID和过期时间过期时间设为15分钟。这样用户在选座这个高频交互环节所有状态变化都走内存不压数据库。Boolean locked redisTemplate.opsForValue() .setIfAbsent(buildSeatLockKey(sessionId, seatCode), userId.toString(), Duration.ofMinutes(15)); if (Boolean.FALSE.equals(locked)) { throw new BizException(座位刚刚被别人锁定请重新选择); }数据库层面订单表加了一个uk_session_seat唯一索引字段是session_id seat_code。万一Redis过期或重启丢了数据数据库的唯一索引也能阻止同一座位产生两条有效订单。双保险实测压测50并发没有超卖。2.2 订单超时未支付自动关闭的定时任务方案选座锁定后用户不一定付款如果一直占着座位不放别人就买不到。我用了两个机制配合一是前面说的Redis锁15分钟自动过期二是订单表里的expire_time字段定时任务每分钟扫描一次把超时未支付订单置为取消并手动释放Redis锁。定时任务用Spring自带的Scheduled就够了不需要引入Quartz。注意一个细节扫描条件是status0 AND expire_time NOW()这个查询要给expire_time建索引否则表一大全表扫描很拖慢库。2.3 JWT登录鉴权与前端路由守卫的配合接口不可能裸奔。用户登录成功后后端签发JWT前端存到localStorage每次请求在Axios拦截器里带上Authorization头。后端用拦截器校验token从token里解析userId和角色把用户信息放入ThreadLocal上下文业务层直接取。Vue Router的全局前置守卫根据token和用户角色做跳转。普通用户放行用户端页面管理端页面需要校验角色是admin否则直接踢回登录页。public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { Claims claims JwtUtil.parseToken(token.substring(7)); UserContext.set(claims.get(userId, Long.class), claims.get(role, String.class)); return true; } response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } }JWT不存服务端天然适合横向扩容但也意味着失效只能靠过期时间。我给token设置了2小时有效期前端在响应拦截器里检测401就跳登录页省去了刷新token的复杂度。3. 实操过程SpringBoot后端从零搭建3.1 工程初始化与分层分包目录工程创建我直接用了Spring Initializr依赖勾选Spring Web、MyBatis、MySQL Driver、Lombok、Validation。有人习惯手动加依赖我建议直接在线生成省一遍版本对齐的坑。SpringBoot版本注意别追太高2.7.x和MyBatis starter 2.3.x配合稳定升到3.x后javax命名空间全换成了jakarta踩坑成本不值当。分包结构是当前项目的主心骨按业务域拆包之后每个domain下四层结构com.example.cinema ├── common // 统一返回、异常处理、常量 ├── config // 配置类拦截器、CORS、MyBatis ├── domain │ ├── user │ │ ├── controller │ │ ├── service │ │ ├── mapper │ │ └── entity │ ├── film │ ├── session │ ├── order │ └── pay └── utils // JWT工具、日期工具等3.2 MyBatis配置与XML编写要点MyBatis的配置文件里几个关键开关必须设对。首先是驼峰映射map-underscore-to-camel-case: true否则数据库的create_time映射不到createTime。其次是SQL日志打印开发环境把mybatis.configuration.log-impl设为StdOutImpl看真实执行SQL和参数方便排查生产环境记得关掉每个SQL都打日志很费性能。mybatis: mapper-locations: classpath:/mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl type-aliases-package: com.example.cinema.domainXML里的动态SQL是我重点打磨的地方。场次查询需要支持按影片、按影院、按日期范围筛选直接用ifwhere标签拼条件比注解SQL可读性好得多。MyBatis的foreach遍历也常用批量插入座位锁定时用它insert idbatchInsert INSERT INTO tb_order_seat (order_id, session_id, seat_code) VALUES foreach collectionlist itemitem separator, (#{item.orderId}, #{item.sessionId}, #{item.seatCode}) /foreach /insert3.3 统一返回结果与全局异常处理接口返回格式必须统一不然前端处理起来想骂人。我定义了ResultT封装包含code、message、data三个字段。成功返回code200业务异常code用400开头系统异常500。前端拦截器只看code和HTTP状态码逻辑非常清爽。全局异常处理用RestControllerAdvice捕获业务异常、参数校验异常、兜底异常三类。特别要注意参数校验失败时MethodArgumentNotValidException的message要给前端可读内容否则会抛一长串英文。3.4 事务管理订单创建必须原子性购票订单创建跨越多个表先生成订单主记录再插入座位明细最后扣减场次余票。这三步必须在一个事务里否则中途失败会出现幽灵占座。我在Service层方法上标了Transactional(rollbackFor Exception.class)特别强调rollbackFor要写成Exception.class而不是默认的RuntimeException因为有些业务异常是checked exception默认不触发回滚。数据库连接池用HikariCPSpringBoot自带。配置里maximum-pool-size设成20购票高峰期连接数够用就行没必要开100个每开一个连接都在消耗MySQL的线程资源。4. 实操过程Vue3前端从搭建到联调4.1 Vite创建项目与基础工程结构前端我用的Vite脚手架命令一行搞定npm create vitelatest cinema-frontend -- --template vue npm install vue-router4 pinia element-plus axiosVue3项目里的setup语法糖写起来很舒服。影院购票的页面交互分三层影片列表页、电影详情场次选择页、座位选择页。路由设计为嵌套路由把详情页作为父路由选座页是子路由这样切换选座时详情页数据还能留在内存里不用重新请求。Pinia代替Vuex逻辑更扁平。我建了user store管理token和用户信息、order store管理当前选座状态。用户在座位页刷新order store里数据会丢这一点必须处理选座页在onMounted时如果没有store数据就根据路由参数重新加载场次和座位状态。4.2 Axios请求封装与Token拦截器axios封装是前端联调的重要一环。我在封装文件里统一配置了baseURL指向后端接口前缀请求拦截器加token响应拦截器统一解包Result数据结构遇到401跳转登录页遇到业务错误码可以弹ElMessage提示。一个容易被忽略的细节axios的baseURL如果用相对路径/api打包后nginx直接转发到后端开发环境则用Vite的proxy代理避免跨域。这里顺便解决了本地联调跨域的问题不用后端单独开CORS配置。4.3 座位图组件的实现细节座位图是前端最复杂的组件也是面试官最爱问的部分。我用一个二维数组表示影厅座位矩阵行表示排号列表示座号。每个座位有三种状态available可选、locked已锁、sold已售。后端接口返回一个JSON数组前端渲染时动态生成座位单元格。div v-for(row, rowIndex) in seatMap :keyrowIndex classseat-row span classrow-label{{ rowIndex 1 }}排/span div v-for(seat, colIndex) in row :keycolIndex classseat :class{ seat--locked: seat.status locked, seat--selected: selectedSeats.includes(seat.code), seat--sold: seat.status sold } clicktoggleSeat(seat) {{ seat.code }} /div /div购票规则是单笔订单最多选5张票连续座位优先。我写了一个isSeatAvailable方法检查选中的座位是否连续这算是个业务小亮点给项目加分的细节通常就在这种地方。4.4 打包构建与Nginx反向代理前端构建命令npm run build会产出dist目录里面是纯静态文件。我把dist目录和后端SpringBoot包部署在同一个服务器上Nginx配置监听80端口把/api前缀的请求反向代理到后端8080端口其他路径指向前端静态文件。server { listen 80; server_name cinema.example.com; root /var/www/cinema/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }注意try_files这行Vue Router的历史模式路由必须配置这个否则刷新页面会404。5. 常见问题与排查实录5.1 MySQL连接中的时区和SSL报错项目启动时最容易报的一个错是The server time zone value CST is unrecognized这是MySQL 8.0默认时区和驱动校验不一致造成的。连接参数加上serverTimezoneAsia/Shanghai即可。另外MySQL 8默认开启SSL驱动连接时会报SSL警告甚至报错我直接加了useSSLfalse本地开发完全够用。5.2 MyBatis返回字段全是null这个坑也很经典数据库字段是下划线命名Java属性是驼峰命名但全局驼峰映射没有生效。排查顺序是先看yaml里mybatis.configuration.map-underscore-to-camel-case是否true再看resultType是否指定了别名最后看是不是手写了resultMap覆盖了自动映射。手写resultMap时没列出的字段会映射为null这是最容易忽略的。5.3 选座并发导致的超卖有一次压测发现库存变成负数查日志发现是同一个场次并发创建两条相同座位的订单。原因是Redis锁过期时间设成了5分钟但支付流程走了8分钟锁就提前失效了。后来把锁的过期时间加长到15分钟并且订单关闭时主动释放锁问题解决。这个经历让我养成了一个习惯所有跟锁相关的逻辑必须同时考虑锁超时和业务超时的关系。5.4 前端跨域和刷新404开发环境下前后端端口不同浏览器直接fetch会跨域。在Vite的devServer里配置proxy代理让浏览器看起来请求的是同源路径。部署后刷新404的问题就是上面说的try_files配置缺失。这两个问题几乎每个前后端分离项目都会遇到建议直接作为排查清单固定下来。5.5 其他常见问题速查现象原因解决方案接口405Controller请求方式与前端不一致检查RequestMapping是否限制method数据库连接超时连接池空闲连接被MySQL杀掉加HikariCP的connection-test-queryRedis锁未释放业务异常中断了finally逻辑在try-catch-finally中统一释放日期格式错乱Jackson默认格式不含时分秒增加jackson date-format全局配置Maven依赖冲突Slf4j和Logback版本不统一统一用spring-boot-dependencies管理版本6. 可扩展方向与优化建议如果这个项目要做成真正可上线的系统有几个方向值得扩展。一是把Redis锁升级为Redisson的分布式锁它天然支持看门狗续期不用手动管理过期时间。二是接口层的幂等处理用户重复点击下单按钮通过前端按钮防抖加后端订单号唯一校验双重保证。三是优惠券模块用户购票前领取优惠券下单时校验使用规则这是商业化系统的常见扩展。性能优化上热门的电影详情和场次列表可以加一层Redis缓存缓存key设计成film:detail:{filmId}。数据库层面给查询频率高的字段建立联合索引比如场次表的film_id start_time联合索引能让排片查询走索引而不是全表扫描。支付模块真实对接时建议设计成独立的pay服务接入微信或支付宝时只需要替换支付策略实现类订单状态通过回调异步更新不要同步等待支付结果。这套源码里支付是模拟状态切换真实支付时把Strategy模式的接口留好就行。最后再分享一个小技巧开发这类系统时一定要在本地用Docker把MySQL和Redis跑起来service版本固定避免“我机器上好好的你那边跑不起来”的经典矛盾。我就是吃了好几次环境不一致的亏才把所有依赖全部容器化的。这套影院购票系统从数据库到前端踩过的坑基本涵盖了前后端分离项目的大半常见问题把这些经验和源码对照着看比单纯抄一遍代码收获要大得多。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号