1. 项目概述
电影院售票系统是现代影院运营的核心支撑平台,这个基于SpringBoot的Web应用实现了从影片管理、排期设置到在线选座、支付结算的全流程数字化。我去年为本地一家连锁影院实施这套系统时,深刻体会到传统人工售票模式在高峰时段的力不从心——排队购票的顾客经常因为等待时间过长而流失,而手工统计上座率更是让运营人员头疼不已。
这套系统采用B/S架构,前端使用Thymeleaf模板引擎配合Bootstrap实现响应式布局,后端基于SpringBoot 2.7快速搭建,数据持久层选用MyBatis-Plus提高开发效率。特别在座位锁定机制上,我们通过Redis分布式锁解决了并发选座时的数据竞争问题,实测在500并发请求下仍能保证座位状态的准确性。
2. 核心功能模块设计
2.1 多维度影片管理系统
影片管理模块采用树形分类结构,支持按类型、地区、年代等多维度分类。核心数据表设计如下:
| 表名 | 关键字段 | 索引优化点 |
|---|---|---|
| movie_info | id, title, duration, release_date | title字段建立全文索引 |
| movie_category | category_id, parent_id, name | parent_id建立B+树索引 |
在实现模糊查询时,我们放弃了传统的LIKE语句,改用Elasticsearch构建全文检索:
java复制@Repository
public interface MovieSearchRepository extends ElasticsearchRepository<MovieEsEntity, Long> {
@Query("{\"multi_match\":{\"query\":\"?0\",\"fields\":[\"title^3\",\"actors\"]}}")
Page<MovieEsEntity> findByKeyword(String keyword, Pageable pageable);
}
2.2 动态场次排期算法
排期模块的难点在于避免放映厅的时间冲突,我们开发了智能校验算法:
java复制public boolean checkScheduleConflict(LocalDateTime startTime, int duration, int hallId) {
LocalDateTime endTime = startTime.plusMinutes(duration);
return scheduleMapper.countConflictSchedule(
hallId,
startTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
endTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
) == 0;
}
该算法会考虑影片清洁时间(默认30分钟)和设备调试时间(3D影片需额外20分钟),这些参数都在application.yml中可配置:
yaml复制cinema:
schedule:
cleaning-duration: 30
3d-preparation: 20
2.3 高并发座位锁定机制
选座环节采用Redis+Lua脚本实现原子化操作:
lua复制local key = KEYS[1]
local seats = ARGV[1]
local expire = tonumber(ARGV[2])
for seat in string.gmatch(seats, "[^,]+") do
if redis.call("GET", key..seat) ~= false then
return 0
end
end
for seat in string.gmatch(seats, "[^,]+") do
redis.call("SET", key..seat, "1", "EX", expire)
end
return 1
在SpringBoot中通过JedisCluster执行:
java复制Long result = (Long) jedisCluster.eval(
luaScript,
Collections.singletonList(lockPrefix + scheduleId),
Arrays.asList(selectedSeats, String.valueOf(expireSeconds))
);
3. 关键技术实现细节
3.1 支付模块的防重复提交
采用"Token+时间窗口"双重校验:
- 前端生成UUID作为token存入Session
- 提交时验证token有效性后立即失效
- 相同订单号在5秒内禁止重复请求
java复制@PostMapping("/payment")
public Result payOrder(@RequestParam String token,
@Valid PaymentDTO dto) {
if (!paymentService.verifyToken(token)) {
throw new BusinessException("无效的支付令牌");
}
// 时间窗口检查
if (redisTemplate.opsForValue().get("pay:"+dto.getOrderNo()) != null) {
throw new BusinessException("请勿重复提交支付");
}
redisTemplate.opsForValue().set(
"pay:"+dto.getOrderNo(),
"1",
5, TimeUnit.SECONDS
);
return paymentService.processPayment(dto);
}
3.2 分布式Session管理
使用Spring Session整合Redis实现集群环境下的会话共享:
java复制@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class SessionConfig {
@Bean
public LettuceConnectionFactory connectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName("redis-cluster.example.com");
config.setPort(6379);
return new LettuceConnectionFactory(config);
}
}
同时配置了Session的自动续期机制,当用户活跃时会延长过期时间。
4. 性能优化实战
4.1 二级缓存策略
采用Caffeine+Redis的二级缓存架构:
java复制@Cacheable(value = "movie", key = "#id",
cacheManager = "caffeineCacheManager")
public MovieDetailVO getMovieDetail(Long id) {
MovieDetailVO vo = redisTemplate.opsForValue().get("movie:"+id);
if (vo == null) {
vo = convertToVO(movieMapper.selectById(id));
redisTemplate.opsForValue().set(
"movie:"+id,
vo,
2, TimeUnit.HOURS
);
}
return vo;
}
缓存更新策略:
- 热点数据:Caffeine设置10分钟过期,最大500条
- 全量数据:Redis设置2小时过期,LRU淘汰策略
4.2 数据库分表策略
票务数据按月份水平分表,使用Sharding-JDBC实现:
yaml复制spring:
shardingsphere:
datasource:
names: ds0
sharding:
tables:
ticket_order:
actual-data-nodes: ds0.ticket_order_$->{2023..2030}0$->{1..9},ds0.ticket_order_$->{2023..2030}1$->{0..2}
table-strategy:
standard:
precise-algorithm-class-name: com.example.cinema.sharding.OrderMonthPreciseAlgorithm
range-algorithm-class-name: com.example.cinema.sharding.OrderMonthRangeAlgorithm
5. 安全防护体系
5.1 购票环节的风控措施
- 设备指纹识别:通过JS收集浏览器特征生成唯一指纹
- 行为验证码:滑动拼图+点击验证组合
- 限流策略:
- 用户维度:每分钟20次请求
- IP维度:每小时100次请求
java复制@RateLimiter(value = 20, key = "#userId")
@PostMapping("/book")
public Result bookTicket(@RequestHeader Long userId,
@Valid BookDTO dto) {
// 业务逻辑
}
5.2 敏感数据加密方案
采用国密SM4算法加密用户手机号:
java复制public class SM4Util {
private static final String KEY = "7d22f34e5a68119b";
public static String encrypt(String plainText) {
SM4Engine engine = new SM4Engine();
engine.init(true, new KeyParameter(KEY.getBytes()));
byte[] encrypted = new byte[16];
engine.processBlock(plainText.getBytes(), 0, encrypted, 0);
return Hex.toHexString(encrypted);
}
}
数据库存储的是加密后的密文,查询时使用函数索引:
sql复制CREATE INDEX idx_customer_phone ON customer(SM4_decrypt(phone));
6. 运维监控方案
6.1 健康检查端点
SpringBoot Actuator配置:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
probes:
enabled: true
6.2 日志收集架构
采用ELK+Filebeat方案:
code复制filebeat.prospectors:
- type: log
paths:
- /var/log/cinema/*.log
fields:
app: cinema-ticket
output.logstash:
hosts: ["logstash:5044"]
7. 典型问题排查实录
7.1 座位锁定失效问题
现象:高峰时段出现座位重复售卖
排查过程:
- 检查Redis集群状态,发现节点间同步延迟500ms
- 网络监控显示跨机房流量拥塞
解决方案:
- 调整Redis为同机房部署
- 增加本地缓存作为二级校验
- 超时时间从30秒调整为60秒
7.2 支付回调丢失问题
现象:第三方支付回调有时无法触发订单状态更新
解决方案:
- 增加回调日志表记录原始请求
- 实现定时任务补偿机制
java复制@Scheduled(cron = "0 */5 * * * ?")
public void checkUnconfirmedOrders() {
List<Order> orders = orderMapper.selectUnconfirmed(
LocalDateTime.now().minusMinutes(15));
orders.forEach(order -> {
paymentService.queryPaymentStatus(order.getPaymentNo());
});
}
8. 部署实践
8.1 Docker化部署
Dockerfile关键配置:
dockerfile复制FROM openjdk:11-jre
COPY target/cinema.jar /app/
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app/cinema.jar",
"--spring.profiles.active=prod"]
8.2 蓝绿发布方案
通过Nginx实现流量切换:
nginx复制upstream cinema_blue {
server 192.168.1.101:8080;
}
upstream cinema_green {
server 192.168.1.102:8080;
}
server {
location / {
proxy_pass http://cinema_blue;
}
# 切换时修改为proxy_pass http://cinema_green;
}
在项目实际运行过程中,我们发现三个值得注意的经验:首先,电影海报等静态资源一定要使用CDN加速,我们最初直接存储在服务器本地,导致页面加载时间经常超过3秒;其次,数据库连接池的最大活跃数需要根据实际QPS动态调整,我们通过Arthas监控发现默认配置在高并发时成了瓶颈;最后,短信验证码服务一定要做熔断降级,某次运营商接口故障导致整个购票流程阻塞的教训非常深刻。
