1. 项目概述
最近在开发一个企业级电影院购票系统时,遇到了不少技术挑战和架构设计上的坑。这个基于SpringBoot+Vue+MyBatis的全栈项目,从零开始搭建到最终上线,前后花了近两个月时间。今天就来详细拆解这个系统的技术实现,分享一些实战中积累的经验。
这个系统主要解决传统影院售票的三大痛点:一是线下排队购票效率低下,二是座位资源无法实时可视化,三是会员体系与票务系统割裂。通过前后端分离的架构设计,我们实现了电影排片、在线选座、移动支付、会员积分等核心功能的一体化管理。
2. 技术架构解析
2.1 后端技术选型
选择SpringBoot作为后端框架主要基于以下几个考量:
- 快速启动:通过starter依赖可以快速集成MyBatis、Redis等组件,省去了繁琐的XML配置。比如数据库连接池直接使用spring-boot-starter-data-jpa,三行配置就能搞定:
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/cinema?useSSL=false
username: root
password: 123456
- 性能优化:针对高并发场景做了三点特别处理:
- 使用HikariCP连接池替代默认的Tomcat JDBC
- 二级缓存采用Redis集群,缓存电影列表和场次信息
- 对座位锁定操作使用Redisson分布式锁
- 安全防护:通过Spring Security实现了:
- 基于JWT的认证机制
- 防SQL注入的参数过滤
- XSS攻击防护的全局过滤器
2.2 前端技术方案
Vue.js的选型主要考虑移动端适配和开发效率:
- 核心组件设计:
- 座位选择器采用Canvas绘制,实时同步后端座位状态
- 电影海报墙使用懒加载技术,滚动到可视区域再加载图片
- 支付流程采用单页应用(SPA)模式,避免页面刷新导致状态丢失
- 状态管理:
- 使用Vuex管理全局状态如用户登录信息
- 路由级按需加载提升首屏速度
- 采用axios拦截器统一处理HTTP异常
- 性能优化技巧:
- 静态资源走CDN加速
- 启用Gzip压缩
- 关键CSS内联到HTML头部
3. 数据库设计详解
3.1 核心表结构优化
原始设计中电影表(movie_info)存在几个问题:
- 导演和主演字段用VARCHAR存储,无法建立关联查询
- 电影时长单位不统一(有分钟也有小时)
- 海报URL没有做哈希处理
优化后的方案:
sql复制CREATE TABLE `movie_info` (
`movie_id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`movie_name` VARCHAR(100) NOT NULL,
`director_id` BIGINT COMMENT '关联导演表',
`duration_min` INT UNSIGNED COMMENT '统一用分钟',
`poster_hash` CHAR(32) COMMENT 'MD5值用于缓存控制',
INDEX `idx_name` (`movie_name`),
INDEX `idx_director` (`director_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2 场次表关键设计
场次排期表(schedule_plan)有几个特别注意点:
- 时间处理:
- 使用DATETIME存储精确到分钟的时间
- 通过触发器自动计算end_time(start_time + duration)
- 建立联合索引提高查询效率:
sql复制ALTER TABLE `schedule_plan`
ADD INDEX `idx_movie_time` (`movie_id`, `start_time`);
- 座位锁定机制:
- 剩余座位数更新使用乐观锁:
java复制@Update("UPDATE schedule_plan SET remaining_seats=remaining_seats-1
WHERE schedule_id=#{id} AND remaining_seats>0")
int deductSeat(Long id);
4. 核心功能实现
4.1 在线选座实现
选座功能的技术难点在于并发控制,我们的解决方案:
- 前端实现:
- 使用Canvas动态渲染影厅座位图
- 不同状态座位用颜色区分(可选/已售/锁定)
- 通过WebSocket接收实时座位状态更新
- 后端逻辑:
java复制public Result selectSeats(Long scheduleId, List<String> seats) {
// 1. 获取分布式锁
RLock lock = redissonClient.getLock("LOCK_SCHEDULE:" + scheduleId);
try {
lock.lock(5, TimeUnit.SECONDS);
// 2. 检查座位可用性
List<Seat> occupied = seatMapper.checkOccupied(scheduleId, seats);
if(!occupied.isEmpty()) {
return Result.error("座位已被占用");
}
// 3. 临时锁定座位
seatMapper.lockSeats(scheduleId, seats, LOCK_EXPIRE_SECONDS);
// 4. 生成订单
return createOrder(scheduleId, seats);
} finally {
lock.unlock();
}
}
4.2 支付系统集成
支付流程的关键注意事项:
- 接口安全:
- 使用HTTPS传输
- 参数签名验证
- 金额单位统一用分(避免小数问题)
- 事务处理:
java复制@Transactional
public Result handlePayment(Long orderId) {
// 1. 验证订单状态
Order order = orderMapper.selectByIdForUpdate(orderId);
if(order.getStatus() != OrderStatus.CREATED) {
return Result.error("订单状态异常");
}
// 2. 调用支付接口
PaymentResult result = paymentService.pay(order);
// 3. 更新订单状态
order.setStatus(OrderStatus.PAID);
orderMapper.updateById(order);
// 4. 释放座位锁定
seatMapper.releaseLock(order.getScheduleId(), order.getSeats());
// 5. 增加会员积分
if(order.getUserId() != null) {
memberService.addPoints(order.getUserId(), order.getAmount());
}
}
5. 性能优化实战
5.1 缓存策略
- 多级缓存设计:
- 本地缓存:使用Caffeine缓存热门电影数据(有效期5分钟)
- 分布式缓存:Redis集群存储场次余票信息
- 缓存击穿防护:对热点key使用互斥锁重建
- 缓存更新策略:
java复制@CacheEvict(value = "movies", key = "#movie.movieId")
public void updateMovie(Movie movie) {
movieMapper.updateById(movie);
// 异步更新搜索引擎
searchService.asyncUpdate(movie);
}
5.2 数据库优化
- 索引优化:
- 为订单表添加复合索引:(user_id, create_time)
- 使用覆盖索引避免回表:
sql复制SELECT movie_id, movie_name FROM movie_info
WHERE genre = '动作' AND is_showing = 1;
- SQL优化案例:
java复制// 反例:N+1查询问题
List<Schedule> schedules = scheduleMapper.selectByMovie(movieId);
schedules.forEach(s -> {
s.setHall(hallMapper.selectById(s.getHallId()));
});
// 正例:关联查询
@Select("SELECT s.*, h.hall_name FROM schedule_plan s " +
"LEFT JOIN hall_info h ON s.hall_id = h.hall_id " +
"WHERE s.movie_id = #{movieId}")
List<ScheduleVO> selectWithHall(Long movieId);
6. 安全防护方案
6.1 常见攻击防护
- XSS防护:
- 前端使用vue-sanitize过滤HTML
- 后端统一转义特殊字符
java复制public String escapeHtml(String input) {
return StringEscapeUtils.escapeHtml4(input);
}
- CSRF防护:
- 关键操作使用POST请求
- 添加CSRF Token验证
- SameSite Cookie设置
6.2 数据安全
- 敏感数据加密:
- 用户密码使用BCrypt加密
- 支付信息AES加密存储
- 日志脱敏处理
- 权限控制:
java复制@PreAuthorize("hasRole('ADMIN') or #userId == principal.id")
public User getUser(Long userId) {
return userMapper.selectById(userId);
}
7. 部署实践
7.1 容器化部署
Docker Compose编排方案:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6
ports:
- "6379:6379"
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
frontend:
build: ./frontend
ports:
- "80:80"
7.2 监控方案
- Spring Boot Actuator:
yaml复制management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
- ELK日志收集:
- Filebeat采集容器日志
- Logstash过滤处理
- Kibana可视化展示
8. 踩坑记录
- 座位并发问题:
初期直接使用数据库乐观锁,在高并发时出现超卖。最终解决方案:
- 前端加入排队机制
- 后端使用Redis分布式锁
- 数据库最终一致性校验
- 支付超时处理:
支付接口超时设置不当导致线程阻塞。优化方案:
- 设置合理的连接超时(3s)和读取超时(5s)
- 引入Hystrix熔断机制
- 增加异步支付结果查询
- 缓存一致性问题:
电影信息更新后缓存未及时失效。解决方法:
- 使用@CacheEvict注解
- 设置合理的缓存过期时间
- 重要操作走数据库校验
这个项目让我深刻体会到,一个看似简单的购票系统,背后需要考虑的细节如此之多。特别是在高并发场景下,任何一个环节没处理好都可能导致严重问题。建议开发类似系统时,尽早进行压力测试,不要等到上线才发现性能瓶颈。