1. 项目概述
作为一个在Java全栈开发领域摸爬滚打多年的老码农,今天想和大家分享一个近期完成的影院订票系统项目。这个系统采用SpringBoot+Vue的前后端分离架构,从数据库设计到前后端联调,再到最终部署上线,整个过程踩了不少坑也积累了不少实战经验。
这个系统主要解决传统影院线下购票效率低下的问题,实现了电影信息展示、在线选座购票、订单管理、用户评价等核心功能。相比市面上常见的票务系统,我们特别强化了高并发场景下的座位锁定机制和支付超时处理,在测试环境下可以稳定支撑每秒500+的并发订票请求。
2. 技术选型与架构设计
2.1 技术栈解析
后端技术栈:
- SpringBoot 2.7.3:快速构建RESTful API
- MyBatis-Plus 3.5.1:简化数据库操作
- Redis 6.2:处理高并发座位锁定
- RabbitMQ 3.9:异步处理订单超时
- JWT:用户认证与授权
前端技术栈:
- Vue 2.6 + Element UI:构建管理后台
- Vue 3 + Vant:移动端用户界面
- Axios:处理HTTP请求
- Vuex:状态管理
数据库:
- MySQL 8.0:主业务数据存储
- Redis:缓存和分布式锁
选择这套技术栈主要基于以下考虑:
- SpringBoot的自动配置和起步依赖可以快速搭建项目骨架
- Vue的组件化开发适合构建复杂的单页应用
- MyBatis-Plus的ActiveRecord模式简化了CRUD操作
- Redis的原子操作特性非常适合处理高并发座位锁定
2.2 系统架构设计
系统采用典型的前后端分离架构:
code复制客户端层(Web/App)
↓ ↑ HTTP/HTTPS
API网关层(Spring Cloud Gateway)
↓ ↑
业务服务层(SpringBoot微服务)
↓ ↑
数据访问层(MyBatis-Plus)
↓ ↑
数据存储层(MySQL+Redis)
这种架构的优势在于:
- 前后端可以并行开发
- 前端可以使用更适合的技术栈
- 后端服务可以独立部署和扩展
- 网关层统一处理认证、限流等问题
3. 核心功能实现
3.1 电影管理模块
电影管理是系统的核心模块,包含电影信息CRUD、排片管理等功能。这里重点讲几个关键实现:
电影信息存储设计:
java复制@Entity
@Table(name = "movie")
public class Movie {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(length = 1000)
private String description;
@Column(name = "cover_url")
private String coverUrl;
@Column(name = "duration_minutes")
private Integer durationMinutes;
@Column(name = "release_date")
private LocalDate releaseDate;
// 其他字段和方法...
}
排片管理实现要点:
- 使用TimeSlot表存储影厅的时间段占用情况
- 排片时检查时间冲突
- 提供批量排片接口提高效率
3.2 订票与座位锁定
订票流程是系统最复杂的部分,核心在于解决高并发下的座位冲突问题。我们采用Redis分布式锁+数据库乐观锁的双重保障机制。
座位锁定流程:
- 用户选择座位后,前端发送锁定请求
- 后端生成唯一锁ID,尝试获取Redis锁
- 锁定成功后设置30秒过期时间
- 用户完成支付后释放锁
- 超时未支付自动释放锁
关键代码示例:
java复制public boolean lockSeats(List<Long> seatIds, Long userId) {
String lockKey = "seat_lock:" + StringUtils.join(seatIds, ",");
String lockId = UUID.randomUUID().toString();
// 尝试获取锁
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockId, 30, TimeUnit.SECONDS);
if (locked) {
try {
// 检查座位是否可用
List<Seat> seats = seatService.listByIds(seatIds);
if (seats.stream().anyMatch(s -> s.getStatus() != SeatStatus.AVAILABLE)) {
return false;
}
// 更新座位状态
seatService.lambdaUpdate()
.in(Seat::getId, seatIds)
.set(Seat::getStatus, SeatStatus.LOCKED)
.set(Seat::getLockedBy, userId)
.set(Seat::getLockedAt, LocalDateTime.now())
.update();
return true;
} catch (Exception e) {
redisTemplate.delete(lockKey);
throw e;
}
}
return false;
}
3.3 订单支付流程
支付流程采用状态机模式管理订单状态变迁:
code复制待支付 → 支付中 → 已支付
↘ 已取消
↘ 已超时
使用RabbitMQ延迟队列处理支付超时:
java复制@Bean
public Queue orderDelayQueue() {
return QueueBuilder.durable("order.delay.queue")
.withArgument("x-dead-letter-exchange", "order.event.exchange")
.withArgument("x-dead-letter-routing-key", "order.timeout")
.build();
}
// 订单创建时发送延迟消息
public void createOrder(Order order) {
orderMapper.insert(order);
rabbitTemplate.convertAndSend(
"order.delay.exchange",
"order.delay",
order.getId(),
message -> {
message.getMessageProperties().setDelay(30 * 60 * 1000); // 30分钟
return message;
}
);
}
4. 数据库设计
4.1 核心表结构
电影表(movie):
sql复制CREATE TABLE `movie` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL,
`description` text,
`cover_url` varchar(255) DEFAULT NULL,
`duration_minutes` int DEFAULT NULL,
`release_date` date DEFAULT NULL,
`status` tinyint DEFAULT '1' COMMENT '1-上映中 0-已下架',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_status` (`status`),
KEY `idx_release_date` (`release_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
场次表(screening):
sql复制CREATE TABLE `screening` (
`id` bigint NOT NULL AUTO_INCREMENT,
`movie_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,
`seat_map` text COMMENT 'JSON格式的座位图',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_movie` (`movie_id`),
KEY `idx_time` (`start_time`),
FOREIGN KEY (`movie_id`) REFERENCES `movie` (`id`),
FOREIGN KEY (`hall_id`) REFERENCES `hall` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4.2 索引优化实践
在高并发查询场景下,我们针对以下字段建立了复合索引:
- 电影列表页:
status + release_date - 场次查询:
movie_id + start_time - 订单查询:
user_id + create_time
对于热点数据如电影详情,使用Redis缓存:
java复制@Cacheable(value = "movie", key = "#id")
public Movie getMovieById(Long id) {
return movieMapper.selectById(id);
}
5. 部署与性能优化
5.1 系统部署方案
我们采用Docker Compose进行容器化部署,主要包含以下服务:
- 前端静态资源:Nginx
- 后端应用:SpringBoot Jar
- 数据库:MySQL主从
- 缓存:Redis哨兵
- 消息队列:RabbitMQ集群
docker-compose.yml关键配置:
yaml复制version: '3.8'
services:
backend:
image: openjdk:11-jre
ports:
- "8080:8080"
volumes:
- ./app.jar:/app.jar
command: java -jar /app.jar
depends_on:
- redis
- mysql
redis:
image: redis:6.2-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
mysql:
image: mysql:8.0
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: cinema
volumes:
- mysql_data:/var/lib/mysql
volumes:
redis_data:
mysql_data:
5.2 性能优化措施
-
接口优化:
- 使用Spring Cache注解缓存热点数据
- 批量接口替代循环单条操作
- 异步处理非核心流程(如发送通知)
-
数据库优化:
- 读写分离,查询走从库
- 大表分库分表(用户行为日志)
- 合理使用索引避免全表扫描
-
前端优化:
- 组件懒加载
- 路由懒加载
- 图片懒加载和CDN加速
6. 常见问题与解决方案
6.1 座位冲突问题
现象: 多个用户同时锁定同一座位
解决方案:
- 使用Redis分布式锁保证原子性
- 数据库使用乐观锁更新
- 前端轮询座位状态变化
6.2 支付超时处理
现象: 用户锁定座位后未完成支付
解决方案:
- 订单创建时发送延迟消息
- 消费者处理超时订单
- 提供订单恢复功能(需重新锁定座位)
6.3 高并发下的性能问题
现象: 热门场次开售时系统响应变慢
解决方案:
- 使用Redis缓存场次和座位数据
- 接口限流(Guava RateLimiter)
- 前端加入排队机制
7. 开发心得与建议
在实际开发这个影院系统的过程中,我总结了以下几点经验:
-
分布式事务要谨慎: 座位锁定、订单创建、支付完成等操作涉及多个服务,我们最终采用了最终一致性方案而非强一致性,通过消息队列和定时任务保证数据最终一致。
-
缓存策略要合理: 电影详情这类读多写少的数据适合缓存,但场次座位信息这类高频变化的数据需要谨慎处理,我们采用了短期缓存+版本号的方案。
-
监控不能少: 上线后我们很快发现了一些性能瓶颈,后来接入了Prometheus监控系统,可以实时查看接口响应时间、数据库查询性能等关键指标。
-
压测要提前: 在开发阶段我们就使用JMeter模拟了高并发场景,发现了不少问题,比如数据库连接池配置不合理、Redis连接泄漏等。
对于想开发类似系统的同学,我的建议是从最小可行产品(MVP)开始,先实现核心的订票流程,再逐步扩展其他功能。另外,一定要重视测试,特别是并发场景下的测试。