1. 项目背景与核心价值
去年帮朋友运营的业余足球联赛开发票务系统时,发现市面上现成的解决方案要么功能冗余要么价格虚高。这个基于SpringBoot的球赛购票系统就是当时沉淀下来的实战成果,现已稳定支撑3个省级联赛的票务运营。相比商业系统,它具有三个显著优势:
- 轻量级架构部署成本降低70%(单台2核4G服务器可支撑日均10万访问)
- 定制化开发周期缩短至2周(标准功能模块开箱即用)
- 支持动态票价策略(如根据上座率自动调价)
系统采用经典的三层架构设计,前端Vue.js+后端SpringBoot+MySQL,通过Redis处理高并发选座。下面结合核心模块代码,详解从开发到部署的全流程实践。
2. 系统架构设计解析
2.1 技术栈选型依据
选择SpringBoot2.7作为核心框架主要考虑:
- 内嵌Tomcat简化部署(对比传统SSH架构节省40%启动配置)
- Starter生态完善(集成Redis/JPA/Security等组件仅需添加依赖)
- Actuator监控端点便于运维(特别适合中小型赛事快速迭代)
数据库采用MySQL5.7而非8.0版本,实测在票务场景下:
- 5.7的读写性能差异<5%
- 兼容性更好(部分托管服务尚未支持8.0)
- 内存占用降低20%
2.2 高并发场景解决方案
选座环节采用Redis+Lua脚本实现分布式锁,关键代码片段:
java复制// 座位锁定逻辑
String luaScript = "if redis.call('exists',KEYS[1])==0 then " +
"redis.call('setex',KEYS[1],ARGV[2],ARGV[1]);" +
"return 1;" +
"end;" +
"return 0;";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey),
userId.toString(),
String.valueOf(expireTime));
踩坑记录:初期使用SETNX命令出现死锁,后改用setex+lua保证原子性,超时时间设置为15秒(实测用户平均支付完成时间)
3. 核心业务模块实现
3.1 动态票价算法
票价模型采用基础价格+浮动因子:
code复制最终票价 = 基准价 × (1 + 赛事热度系数) × (1 - 早鸟折扣) + 服务费
其中热度系数通过历史数据训练得出:
java复制// 热度计算示例
public double calculateHeatValue(Long matchId) {
// 访问量权重40%
double pvWeight = visitCount(matchId) * 0.4 / maxVisitCount;
// 收藏量权重30%
var favoriteWeight = favoriteCount(matchId) * 0.3 / maxFavoriteCount;
// 历史上座率权重30%
var attendanceWeight = avgAttendance(matchId) * 0.3;
return pvWeight + favoriteWeight + attendanceWeight;
}
3.2 购票状态机设计
使用Spring StateMachine处理订单流转:
mermaid复制stateDiagram
[*] --> UNPAID
UNPAID --> PAID: 支付成功
UNPAID --> CANCELLED: 用户取消
PAID --> COMPLETED: 检票入场
PAID --> REFUNDING: 申请退款
REFUNDING --> REFUNDED: 退款成功
注意事项:状态变更必须加@Transactional注解,避免并发修改导致状态不一致
4. 部署实战与优化
4.1 服务器配置建议
实测性能数据(阿里云ECS):
| 配置规格 | 并发承载 | 平均响应时间 |
|---|---|---|
| 2核4G | 1500QPS | 230ms |
| 4核8G+Redis缓存 | 5000QPS | 120ms |
关键JVM参数:
bash复制-server -Xms2048m -Xmx2048m -XX:MaxMetaspaceSize=512m
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
4.2 灰度发布方案
通过Nginx实现流量切分:
nginx复制upstream backend {
server 192.168.1.10:8080 weight=90; # 旧版本
server 192.168.1.20:8080 weight=10; # 新版本
}
location /api {
proxy_pass http://backend;
proxy_set_header X-Forwarded-For $remote_addr;
}
5. 典型问题排查手册
5.1 选座冲突问题
现象:多个用户同时看到同一座位可用
排查步骤:
- 检查Redis锁过期时间(应>支付超时时间)
- 验证Lua脚本执行返回值(返回0表示锁失败)
- 监控Redis内存使用(避免内存不足导致锁失效)
5.2 支付回调丢失
解决方案:
- 增加本地任务表记录支付状态
- 定时补偿查询(每5分钟扫描UNPAID订单)
- 接入支付宝/微信的补单接口
java复制@Scheduled(cron = "0 */5 * * * ?")
public void checkPaymentStatus() {
unpaidOrders.forEach(order -> {
boolean paid = paymentService.queryThirdParty(order.getId());
if(paid) orderService.updateToPaid(order.getId());
});
}
6. 源码结构说明
项目采用模块化设计:
code复制ticket-system
├── ticket-admin // 管理后台
├── ticket-api // 接口模块
├── ticket-common // 通用工具
├── ticket-job // 定时任务
└── ticket-web // 用户门户
关键配置项:
- application-redis.yml:缓存配置
- application-dynamic.yml:动态参数
- application-cluster.yml:集群部署配置
实际部署时发现,将静态资源与API服务分离可提升30%吞吐量。建议使用Nginx处理静态文件,SpringBoot专注业务逻辑。这个项目经过三次大型赛事验证,最新增加了基于观众位置的智能推荐功能,后续可以考虑接入更多数据分析模块。