1. 项目概述与背景
作为一个在Java Web开发领域摸爬滚打多年的老码农,我最近完成了一个在线电影票务系统的完整开发。这个项目让我深刻体会到,一个看似简单的购票流程背后,其实隐藏着复杂的业务逻辑和技术挑战。今天我就把这个项目的实战经验完整分享出来,希望能给正在做类似项目的同行一些参考。
在线票务系统本质上是一个典型的高并发实时交易系统,需要同时解决以下几个核心问题:
- 座位资源的实时同步与锁定机制
- 支付流程的时效性与数据一致性
- 突发流量下的系统稳定性
- 多角色(用户/影院/管理员)的权限管理
2. 技术架构设计
2.1 整体技术栈选型
经过多方对比,最终确定的技术方案如下:
前端技术栈:
- 基础:HTML5 + CSS3 + JavaScript
- 框架:jQuery + Bootstrap 4.6
- 模板引擎:JSP 2.3
- 图表库:ECharts(用于后台数据可视化)
后端技术栈:
- 核心框架:Java Servlet 4.0
- ORM框架:MyBatis 3.5.7
- 连接池:HikariCP 4.0.3
- 日志系统:Log4j 2.17.1
- 工具类:Apache Commons系列
数据库:
- 主库:MySQL 8.0.28(InnoDB引擎)
- 缓存:Redis 6.2.6(座位状态缓存)
服务器环境:
- Web容器:Tomcat 9.0.64
- 构建工具:Maven 3.8.4
- 版本控制:Git 2.35.1
技术选型心得:Servlet虽然看起来"老旧",但对于教学项目和企业级应用来说,它足够轻量且易于理解。实际项目中我们团队曾对比过Spring Boot,最终选择Servlet是考虑到:1) 学习曲线平缓 2) 部署简单 3) 对服务器资源要求低。
2.2 系统分层架构
采用经典的MVC模式,但做了适当改良:
code复制表现层(View)
├── JSP页面
├── 静态资源
└── AJAX接口
控制层(Controller)
├── Servlet
├── 过滤器(Filter)
└── 监听器(Listener)
业务层(Service)
├── 接口定义
└── 实现类
持久层(DAO)
├── MyBatis Mapper
└── 实体类(Entity)
基础设施层
├── 工具类
├── 配置管理
└── 异常处理
3. 核心模块实现
3.1 用户认证模块
3.1.1 密码安全处理
采用加盐哈希存储密码,关键实现代码:
java复制public class PasswordUtil {
private static final int SALT_LENGTH = 16;
private static final int HASH_ITERATIONS = 1024;
public static String generateSalt() {
SecureRandom random = new SecureRandom();
byte[] salt = new byte[SALT_LENGTH];
random.nextBytes(salt);
return Hex.encodeHexString(salt);
}
public static String hashPassword(String password, String salt) {
PBEKeySpec spec = new PBEKeySpec(
password.toCharArray(),
salt.getBytes(),
HASH_ITERATIONS,
256
);
// ... 省略实现细节
}
}
3.1.2 会话管理
使用Redis存储会话信息,解决分布式环境下的会话共享问题:
java复制public class RedisSessionManager {
private static final int SESSION_EXPIRE = 1800; // 30分钟
public void createSession(HttpServletRequest request, User user) {
String sessionId = UUID.randomUUID().toString();
Jedis jedis = RedisPool.getResource();
try {
jedis.hset("session:"+sessionId, "userId", user.getId().toString());
jedis.expire("session:"+sessionId, SESSION_EXPIRE);
request.getSession().setAttribute("SESSION_ID", sessionId);
} finally {
jedis.close();
}
}
}
3.2 座位锁定机制
这是系统最复杂的部分之一,我们实现了三级锁定策略:
- 前端伪锁定:用户选择座位时,前端标记为"选择中"状态
- 服务端软锁定:提交订单时,Redis设置15分钟过期锁
- 数据库硬锁定:支付成功后,更新数据库座位状态
关键Redis操作代码:
java复制public boolean lockSeats(List<Long> seatIds, Long userId) {
String lockKey = "seat_lock:" + StringUtils.join(seatIds, ",");
Jedis jedis = RedisPool.getResource();
try {
// 使用SETNX实现分布式锁
Long result = jedis.setnx(lockKey, userId.toString());
if (result == 1L) {
jedis.expire(lockKey, 900); // 15分钟过期
return true;
}
return false;
} finally {
jedis.close();
}
}
4. 数据库设计优化
4.1 核心表结构
场次表(screening)关键字段优化:
sql复制CREATE TABLE `screening` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`movie_id` bigint(20) NOT NULL COMMENT '电影ID',
`hall_id` bigint(20) NOT NULL COMMENT '影厅ID',
`start_time` datetime NOT NULL COMMENT '开始时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
`price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '票价',
`available_seats` int(11) NOT NULL COMMENT '可用座位数',
`version` int(11) NOT NULL DEFAULT '0' COMMENT '乐观锁版本号',
PRIMARY KEY (`id`),
KEY `idx_movie` (`movie_id`),
KEY `idx_hall_time` (`hall_id`,`start_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4.2 查询性能优化
针对热门场次查询,我们使用了多级缓存策略:
- 本地缓存:使用Caffeine缓存最近5分钟的场次信息
- Redis缓存:存储座位实时状态
- 数据库:最终数据持久化
场次查询的Service层实现:
java复制public List<Screening> getAvailableScreenings(Long movieId) {
// 先查本地缓存
String cacheKey = "screenings:" + movieId;
List<Screening> screenings = localCache.getIfPresent(cacheKey);
if (screenings == null) {
// 查Redis
String json = redisTemplate.opsForValue().get(cacheKey);
if (json != null) {
screenings = JSON.parseArray(json, Screening.class);
} else {
// 查数据库
screenings = screeningDao.findByMovieId(movieId);
// 写入Redis,设置5分钟过期
redisTemplate.opsForValue().set(
cacheKey,
JSON.toJSONString(screenings),
5, TimeUnit.MINUTES
);
}
// 写入本地缓存
localCache.put(cacheKey, screenings);
}
return screenings;
}
5. 支付系统实现
5.1 支付流程设计
采用状态机模式管理订单状态:
code复制待支付 --支付成功--> 已支付
待支付 --超时未支付--> 已取消
已支付 --用户退款--> 已退款
状态转换的核心逻辑:
java复制public boolean changeOrderStatus(Long orderId, OrderStatus from, OrderStatus to) {
String sql = "UPDATE `order` SET status = ? WHERE id = ? AND status = ?";
int rows = jdbcTemplate.update(sql, to.getValue(), orderId, from.getValue());
return rows > 0;
}
5.2 支付超时处理
使用延迟队列处理未支付订单:
java复制public void processExpiredOrders() {
String sql = "SELECT id FROM `order` WHERE status = 0 AND create_time < ?";
Date expireTime = new Date(System.currentTimeMillis() - 30*60*1000);
List<Long> orderIds = jdbcTemplate.queryForList(sql, Long.class, expireTime);
orderIds.forEach(orderId -> {
if (changeOrderStatus(orderId, OrderStatus.PENDING, OrderStatus.CANCELLED)) {
// 释放座位
seatService.unlockSeatsByOrder(orderId);
// 记录日志
log.info("订单超时取消:{}", orderId);
}
});
}
6. 系统安全防护
6.1 常见攻击防护
SQL注入防护:
- 全部使用PreparedStatement
- MyBatis使用#{}语法
XSS防护:
- 前端:使用DOMPurify过滤输入
- 后端:使用Apache Commons Text的StringEscapeUtils
CSRF防护:
java复制public class CsrfFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
HttpServletRequest request = (HttpServletRequest) req;
if ("POST".equalsIgnoreCase(request.getMethod())) {
String sessionToken = (String) request.getSession().getAttribute("CSRF_TOKEN");
String requestToken = request.getParameter("csrfToken");
if (sessionToken == null || !sessionToken.equals(requestToken)) {
throw new RuntimeException("CSRF验证失败");
}
}
chain.doFilter(req, res);
}
}
7. 性能优化实践
7.1 数据库优化
索引优化:
- 为所有外键字段添加索引
- 为常用查询条件组合建立联合索引
SQL优化示例:
sql复制-- 优化前
SELECT * FROM seat WHERE hall_id = ? AND status = 0;
-- 优化后
SELECT id, row_num, col_num FROM seat
WHERE hall_id = ? AND status = 0
ORDER BY row_num, col_num;
7.2 JVM调优
Tomcat启动参数配置:
code复制-server
-Xms1024m
-Xmx2048m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:ParallelGCThreads=4
-XX:ConcGCThreads=2
8. 部署架构
8.1 生产环境部署方案
code复制 +-----------------+
| Nginx 1.21 |
| (负载均衡/静态资源) |
+--------+--------+
|
+----------------+----------------+
| | |
+------+------+ +------+------+ +------+------+
| Tomcat节点1 | | Tomcat节点2 | | Tomcat节点3 |
+------+------+ +------+------+ +------+------+
| | |
+----------------+----------------+
|
+--------+--------+
| MySQL 8.0 |
| (主从复制架构) |
+--------+--------+
|
+--------+--------+
| Redis 6.2 |
| (哨兵模式) |
+-----------------+
8.2 监控方案
- 应用监控:Prometheus + Grafana
- 日志收集:ELK Stack
- APM:SkyWalking
9. 踩坑经验分享
9.1 并发问题排查
问题现象:
在高并发测试时,偶尔会出现座位重复售出的情况。
排查过程:
- 检查日志发现多个请求几乎同时通过座位可用性检查
- 数据库查询确实存在超卖现象
- 事务隔离级别已经是REPEATABLE_READ
解决方案:
java复制// 使用乐观锁控制
public boolean bookSeat(Long seatId, Long userId) {
Seat seat = seatDao.findById(seatId);
if (seat.getStatus() != 0) {
return false;
}
int rows = seatDao.updateStatusWithVersion(
seatId,
0, 1, // 从可用变为锁定
seat.getVersion()
);
return rows > 0;
}
9.2 缓存一致性问题
问题现象:
管理员修改场次信息后,用户端看到的还是旧数据。
解决方案:
采用"先更新数据库,再删除缓存"策略:
java复制public void updateScreening(Screening screening) {
// 1. 更新数据库
screeningDao.update(screening);
// 2. 删除缓存
redisTemplate.delete("screening:" + screening.getId());
// 3. 删除关联缓存
redisTemplate.delete("screenings:" + screening.getMovieId());
}
10. 项目扩展方向
- 微服务改造:将系统拆分为用户服务、订单服务、支付服务等
- 大数据分析:接入Flink实现实时票房分析
- 推荐系统:基于用户历史行为实现个性化推荐
- 小程序端:开发微信小程序版本
- 国际化支持:多语言、多时区适配
这个项目从设计到上线历时3个月,期间遇到了各种预料之外的问题,但最终都一一解决了。最大的体会是:在高并发场景下,任何一个看似简单的功能都可能隐藏着复杂的技术挑战。建议开发类似系统的同行,一定要在早期就重视并发控制和数据一致性问题。