1. 项目概述:SpringBoot航班预订系统设计与实现
每逢春运、暑运和黄金周,民航售票系统总要面临巨大的访问压力。传统售票窗口排起长龙,电话客服占线严重,旅客往往需要花费数小时才能完成一张机票的购买。作为一名经历过多次系统崩溃的开发者,我深知一个稳定可靠的在线机票预订系统对旅客体验的重要性。本次基于SpringBoot的航班预订系统开发,正是为了解决这些痛点问题。
这个系统采用SpringBoot 2.7 + MySQL 8.0的技术栈,实现了从航班查询、在线选座、电子支付到退改签的全流程线上化。系统最核心的价值在于:通过合理的架构设计,能够承受节假日期间10倍于日常的流量冲击,同时保证交易数据的一致性和安全性。我在开发过程中特别注重解决高并发场景下的库存超卖问题和分布式事务一致性难题,这些经验对同类电商系统的开发都有参考价值。
2. 系统架构设计与技术选型
2.1 整体技术架构解析
系统采用经典的三层架构设计,但针对机票预订场景做了特殊优化:
code复制表示层(Web Layer)
├── Vue 3 + Element Plus (前端框架)
└── Thymeleaf (服务端模板)
业务层(Service Layer)
├── Spring Boot 2.7 (核心框架)
├── Spring Security (认证授权)
├── Spring Transaction (事务管理)
└── Custom Business Logic (自定义业务逻辑)
数据层(Data Layer)
├── MySQL 8.0 (主数据库)
├── Redis 7.0 (缓存/分布式锁)
└── MyBatis-Plus 3.5 (ORM框架)
选择这套技术栈主要基于以下考量:
- SpringBoot的快速开发能力:通过自动配置和起步依赖,可以快速搭建具备生产级特性的应用
- Vue+Element Plus的前端组合:提供了丰富的UI组件和响应式设计,适合构建管理后台
- MySQL 8.0的JSON支持:便于存储航班动态信息这类半结构化数据
- Redis的多重用途:既作为缓存减轻数据库压力,又实现分布式锁防止超卖
2.2 数据库设计关键点
机票系统的数据库设计有几个需要特别注意的地方:
航班表(flight)的核心字段:
sql复制CREATE TABLE `flight` (
`id` bigint NOT NULL AUTO_INCREMENT,
`flight_no` varchar(20) NOT NULL COMMENT '航班号',
`departure_city` varchar(50) NOT NULL,
`arrival_city` varchar(50) NOT NULL,
`departure_time` datetime NOT NULL,
`arrival_time` datetime NOT NULL,
`total_seats` int NOT NULL COMMENT '总座位数',
`available_seats` int NOT NULL COMMENT '可用座位数',
`price` decimal(10,2) NOT NULL,
`status` tinyint NOT NULL COMMENT '1-可预订 2-已售罄 3-已取消',
`route_info` json DEFAULT NULL COMMENT '途径站点信息',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_flight_no` (`flight_no`),
KEY `idx_departure` (`departure_city`,`departure_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
订单表(order)的特殊设计:
sql复制CREATE TABLE `order` (
`id` bigint NOT NULL AUTO_INCREMENT,
`order_no` varchar(32) NOT NULL COMMENT '订单编号',
`user_id` bigint NOT NULL,
`flight_id` bigint NOT NULL,
`seat_no` varchar(10) NOT NULL COMMENT '座位号',
`amount` decimal(10,2) NOT NULL COMMENT '实际支付金额',
`status` tinyint NOT NULL COMMENT '1-待支付 2-已支付 3-已取消 4-已退款',
`create_time` datetime NOT NULL,
`pay_time` datetime DEFAULT NULL,
`version` int NOT NULL DEFAULT '0' COMMENT '乐观锁版本号',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_order_no` (`order_no`),
KEY `idx_user` (`user_id`),
KEY `idx_flight` (`flight_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
特别注意:订单表设计了version字段实现乐观锁,这是解决并发修改问题的关键。同时为所有常用查询条件建立了索引,确保查询性能。
3. 核心功能实现细节
3.1 高并发机票预订实现
机票预订的核心难点在于防止超卖和保证系统响应速度。我们采用多级缓存的策略:
-
Redis缓存航班余量:使用Hash结构存储各航班的剩余座位数
java复制// 缓存数据结构示例 redisTemplate.opsForHash().put("flight:seats", flightId, availableSeats); -
分布式锁控制库存扣减:
java复制public boolean bookFlight(Long flightId, int seats) { String lockKey = "lock:flight:" + flightId; try { // 尝试获取分布式锁 boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "locked", 30, TimeUnit.SECONDS); if (!locked) { return false; } // 检查余量 Integer available = (Integer)redisTemplate.opsForHash() .get("flight:seats", flightId.toString()); if (available == null || available < seats) { return false; } // 扣减缓存库存 redisTemplate.opsForHash() .increment("flight:seats", flightId.toString(), -seats); // 异步落库 asyncService.submit(() -> { flightService.reduceSeats(flightId, seats); }); return true; } finally { redisTemplate.delete(lockKey); } } -
最终一致性保证:通过定时任务核对缓存与数据库的库存数据
3.2 订单支付状态机设计
订单状态流转是系统的另一个核心逻辑。我们采用状态机模式保证状态转换的合法性:
java复制public enum OrderStatus {
PENDING_PAYMENT(1, "待支付") {
@Override
public boolean canTransferTo(OrderStatus nextStatus) {
return nextStatus == PAID || nextStatus == CANCELLED;
}
},
PAID(2, "已支付") {
@Override
public boolean canTransferTo(OrderStatus nextStatus) {
return nextStatus == REFUNDED;
}
},
// 其他状态...
private final int code;
private final String desc;
public abstract boolean canTransferTo(OrderStatus nextStatus);
// 状态转换方法
public static boolean canTransfer(OrderStatus from, OrderStatus to) {
return from.canTransferTo(to);
}
}
使用示例:
java复制public boolean changeOrderStatus(Long orderId, OrderStatus newStatus) {
Order order = orderDao.selectById(orderId);
if (OrderStatus.canTransfer(order.getStatus(), newStatus)) {
order.setStatus(newStatus);
return orderDao.updateById(order) > 0;
}
return false;
}
4. 系统优化与安全保障
4.1 性能优化实践
-
SQL优化:针对高频查询添加适当的索引,避免全表扫描
sql复制ALTER TABLE `order` ADD INDEX `idx_user_status` (`user_id`, `status`); -
连接池配置:根据压测结果调整Druid连接池参数
yaml复制spring: datasource: druid: initial-size: 5 min-idle: 5 max-active: 50 max-wait: 1000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 -
缓存策略:采用多级缓存架构
code复制
浏览器缓存 → CDN缓存 → Nginx缓存 → 应用缓存 → Redis缓存 → 数据库
4.2 安全防护措施
-
防SQL注入:坚持使用预编译语句
java复制// 错误做法 String sql = "SELECT * FROM user WHERE username = '" + username + "'"; // 正确做法 Query query = entityManager.createQuery( "SELECT u FROM User u WHERE u.username = :username"); query.setParameter("username", username); -
XSS防护:前端使用vue-sanitize,后端统一过滤
java复制public String sanitize(String input) { return HtmlUtils.htmlEscape(input); } -
CSRF防护:Spring Security默认启用CSRF保护
java复制@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); }
5. 部署与监控方案
5.1 生产环境部署
推荐使用Docker Compose进行容器化部署:
yaml复制version: '3'
services:
app:
image: airline-booking:1.0
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- redis
- mysql
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=yourpassword
- MYSQL_DATABASE=airline
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:7.0
ports:
- "6379:6379"
volumes:
mysql_data:
5.2 监控与告警
-
Spring Boot Actuator:暴露健康检查端点
yaml复制management: endpoints: web: exposure: include: health,info,metrics -
Prometheus + Grafana:构建监控看板
yaml复制# application.yml management: metrics: export: prometheus: enabled: true -
日志收集:采用ELK栈
java复制// logback-spring.xml <appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender"> <destination>logstash:5044</destination> <encoder class="net.logstash.logback.encoder.LogstashEncoder" /> </appender>
6. 开发心得与避坑指南
在实际开发过程中,我总结了以下几个关键经验:
-
库存扣减的原子性问题:
- 错误做法:先查询再更新,会导致超卖
- 正确做法:使用CAS乐观锁或分布式锁
sql复制UPDATE flight SET available_seats = available_seats - 1 WHERE id = ? AND available_seats >= 1 -
事务失效的常见场景:
- 自调用问题:同一个类中方法调用不会走代理
- 异常捕获问题:catch住异常但没有重新抛出
- 非public方法:Spring事务只对public方法有效
-
缓存一致性的解决思路:
- 先更新数据库,再删除缓存
- 设置合理的缓存过期时间
- 使用消息队列异步更新缓存
-
日期处理的坑:
- 前端统一使用ISO8601格式:'YYYY-MM-DDTHH:mm:ss.SSSZ'
- 数据库存储UTC时间
- 根据用户时区显示本地时间
这个系统从设计到实现大约花费了6周时间,其中最大的挑战是高并发场景下的数据一致性问题。通过引入Redis分布式锁和消息队列最终解决了这个问题。建议开发类似系统的同学一定要在早期就考虑好这些边界情况,不要等到用户量上来后再补救。