尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于SSM框架的火车票预售系统:高并发锁机制与事务管理实战

基于SSM框架的火车票预售系统:高并发锁机制与事务管理实战 如果你是一名Java开发者或者正在为毕业设计寻找一个既有商业价值又能体现技术综合性的项目那么“火车票预售系统”很可能已经出现在你的备选清单里。这个选题看似传统但每年都能成为热门背后原因很简单它几乎涵盖了企业级Java Web开发的所有核心模块——用户管理、复杂业务逻辑、事务处理、数据库设计和高并发场景的初步应对。然而很多同学在真正动手时会陷入两个极端要么被SSMSpringSpringMVCMyBatis框架的配置细节淹没做了一个“空架子”要么只关注前端页面后台逻辑脆弱不堪经不起推敲。这篇文章要解决的正是这个矛盾。我将基于一个典型的“基于SSM框架的火车票在线预订与退改签管理系统”毕设项目为你拆解从零到一实现的核心路径。本文不会止步于简单的CRUD增删改查而是会深入探讨几个毕业设计答辩中老师最看重的技术点如何设计一个支持高并发查询与悲观锁定的票务库存模型如何优雅地处理退改签业务中的分布式事务问题以及如何利用Spring的声明式事务管理来保证数据一致性读完本文你将获得一个思路清晰、结构完整、并且具备一定技术深度的项目实现方案可以直接用于你的毕业设计或者作为SSM框架进阶学习的实战案例。1. 为什么火车票预售系统是绝佳的Java毕设选题选择火车票系统作为毕设远不止因为它“常见”。从技术评估和展示的角度它是一个含金量很高的选择。首先它的业务场景为所有人所熟知这让你在答辩陈述时无需花费大量时间解释业务背景可以聚焦于技术实现。其次它的业务复杂度适中且层次分明非常适合分层展示你的技术栈。一个完整的火车票预售系统至少包含以下模块这正好对应了SSM框架的各层职责用户模块注册、登录、权限管理普通用户、管理员。这适合用Spring MVC处理请求Spring Security或拦截器进行权限控制。车次与票务模块车次信息、站点、座位类型、库存管理。这是核心涉及复杂的数据库表设计如车次表、站点表、余票表。MyBatis在这里能充分发挥其灵活SQL映射的优势处理多表关联查询。订单模块创建订单订票、支付模拟、订单查询。这里会大量用到Spring的声明式事务管理因为订票涉及余票更新和订单创建必须是一个原子操作。退改签模块这是体现业务逻辑复杂度的关键。退票、改签涉及费用计算、规则判断如发车时间限制、库存恢复以及可能的事务补偿是展示你解决复杂业务问题能力的绝佳场景。更重要的是这个项目能自然地引出高并发和锁机制的讨论。当多个用户同时预订同一车次的最后几张票时如何防止超卖你可以通过实现数据库悲观锁SELECT ... FOR UPDATE或乐观锁版本号控制来应对这在答辩时是一个重要的技术亮点。因此这个项目不仅是一个功能实现更是一个展示你理解企业级开发中核心难题的舞台。2. 核心概念与系统架构设计在动手编码前厘清几个核心概念和整体架构至关重要这能避免后期大量的返工。2.1 核心业务概念澄清车次 vs. 列车车次如G101是一个逻辑概念代表一条运行线路而列车是物理实体。我们的系统主要管理车次。站点与区间车次途经多个站点。票务库存通常按“区间”如“北京南-上海虹桥”管理。一张从A到D的票会占用A-B、B-C、C-D等多个区间的库存。座位类型与库存同一车次不同座位类型一等座、二等座是独立的库存池。库存的扣减和恢复是系统最核心、最易出错的逻辑。订单状态流订单通常经历待支付-已支付-已出票-已出行/已退票/已改签等状态。清晰的状态机设计是业务稳健的基础。2.2 系统技术架构SSM三层架构我们采用经典的SSM三层架构这是Java Web项目最主流、最清晰的结构。表现层 (Presentation Layer): Spring MVC ├── 处理HTTP请求/响应 ├── 参数绑定与验证 └── 视图解析如返回JSON或JSP页面 业务逻辑层 (Service Layer): Spring Framework ├── 实现核心业务规则 ├── 管理事务边界Transactional └── 协调多个DAO操作 数据访问层 (DAO Layer): MyBatis ├── 封装所有数据库操作 ├── 通过XML或注解编写SQL └── 实现对象-关系映射(ORM)为什么是SSMSpring提供了强大的IoC控制反转和AOP面向切面编程容器管理对象生命周期和事务Spring MVC是轻量级的Web框架配置简单MyBatis则避免了Hibernate等全自动ORM的“黑盒”问题让开发者对SQL有完全的控制力这对于性能要求较高的票务查询和更新操作尤为重要。2.3 数据库表结构关键设计数据库设计是项目的基石。以下是几个核心表的设计要点1. 车次表 (train)CREATE TABLE train ( id int(11) NOT NULL AUTO_INCREMENT, train_number varchar(20) NOT NULL COMMENT 车次号如G101, start_station varchar(50) NOT NULL COMMENT 始发站, end_station varchar(50) NOT NULL COMMENT 终点站, departure_time time NOT NULL COMMENT 发车时间, arrival_time time NOT NULL COMMENT 到达时间, total_seats int(11) DEFAULT NULL COMMENT 总席位数可选, PRIMARY KEY (id), UNIQUE KEY uniq_train_number (train_number) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车次基本信息表;2. 余票表 (ticket_inventory) - 核心难点这是实现票务库存管理的关键。一种常见的设计是为每个车次、每个座位类型、每个区间段创建一条记录。CREATE TABLE ticket_inventory ( id int(11) NOT NULL AUTO_INCREMENT, train_id int(11) NOT NULL COMMENT 关联车次ID, seat_type varchar(10) NOT NULL COMMENT 座位类型如FIRST_CLASS, SECOND_CLASS, from_station_id int(11) NOT NULL COMMENT 出发站ID, to_station_id int(11) NOT NULL COMMENT 到达站ID, total int(11) NOT NULL DEFAULT 0 COMMENT 该区间总票数, available int(11) NOT NULL DEFAULT 0 COMMENT 可用票数, version int(11) NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, PRIMARY KEY (id), KEY idx_train_route (train_id,from_station_id,to_station_id,seat_type), CONSTRAINT fk_inventory_train FOREIGN KEY (train_id) REFERENCES train (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT余票库存表;关键字段解释available实时可售票数。所有订票、退票操作都围绕它进行。version用于实现乐观锁。每次更新时先检查版本号更新成功后版本号1防止更新丢失。3. 订单表 (order)CREATE TABLE order ( id varchar(32) NOT NULL COMMENT 订单号使用分布式ID生成器, user_id int(11) NOT NULL COMMENT 用户ID, train_id int(11) NOT NULL COMMENT 车次ID, seat_type varchar(10) NOT NULL COMMENT 座位类型, from_station_id int(11) NOT NULL, to_station_id int(11) NOT NULL, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0待支付1已支付2已出票3已完成4已退票5已改签, amount decimal(10,2) NOT NULL COMMENT 订单金额, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_user (user_id), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;3. 开发环境准备与项目搭建工欲善其事必先利其器。一个统一的开发环境能避免很多“在我电脑上好好的”之类的问题。3.1 环境与工具清单JDK: 版本 8 或 11推荐11注意与后续工具兼容。确保JAVA_HOME环境变量配置正确。IDE: IntelliJ IDEA社区版或旗舰版或 Eclipse。IDEA对Maven和Spring的支持更友好。构建工具: Apache Maven 3.6。用于管理项目依赖。数据库: MySQL 5.7 或 8.0。这是最常用的关系型数据库。应用服务器: 内嵌Tomcat通过Spring Boot或外置Tomcat 9。版本控制: Git。强烈建议从项目开始就使用Git管理代码。3.2 使用Spring Boot快速初始化SSM项目虽然传统SSM项目需要手动整合很多XML配置但现在更推荐使用Spring Boot来快速搭建。它简化了配置并且内嵌了Tomcat服务器。你可以通过 Spring Initializr 网站或IDE的创建向导来生成项目骨架。需要选择的依赖包括Spring Web: 包含Spring MVC。MyBatis Framework: MyBatis集成。MySQL Driver: MySQL数据库驱动。Lombok(可选): 简化Java Bean的Getter/Setter编写。生成的pom.xml核心依赖部分如下!-- pom.xml -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version !-- 选择一个稳定的版本 -- relativePath/ /parent dependencies !-- Web支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis集成 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.0/version /dependency !-- MySQL驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- 简化POJO -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- 测试 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies3.3 数据库连接与MyBatis配置在src/main/resources/application.yml中配置数据库和MyBatis# application.yml spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/train_ticket_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: your_username password: your_password # MyBatis配置 mybatis: # mapper.xml文件位置 mapper-locations: classpath:mapper/*.xml # 实体类包路径 type-aliases-package: com.yourdomain.trainsystem.entity configuration: # 开启驼峰命名自动映射数据库字段user_name - 实体属性userName map-underscore-to-camel-case: true # 打印SQL日志开发环境方便调试 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl4. 核心业务模块实现拆解接下来我们深入到最核心的票务预订和退改签逻辑的实现。这是整个系统的“发动机”。4.1 实体类与Mapper层创建首先根据数据库设计创建对应的Java实体类。使用Lombok的Data注解可以省去getter/setter代码。// 文件路径src/main/java/com/yourdomain/trainsystem/entity/TicketInventory.java package com.yourdomain.trainsystem.entity; import lombok.Data; import java.io.Serializable; Data public class TicketInventory implements Serializable { private Integer id; private Integer trainId; private String seatType; // 如 FIRST_CLASS private Integer fromStationId; private Integer toStationId; private Integer total; private Integer available; private Integer version; // 乐观锁版本号 }然后创建MyBatis的Mapper接口和对应的XML映射文件。// 文件路径src/main/java/com/yourdomain/trainsystem/mapper/TicketInventoryMapper.java package com.yourdomain.trainsystem.mapper; import com.yourdomain.trainsystem.entity.TicketInventory; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; public interface TicketInventoryMapper { /** * 根据车次、座位类型和区间查询余票信息使用悲观锁 FOR UPDATE */ TicketInventory selectForUpdate(Param(trainId) Integer trainId, Param(seatType) String seatType, Param(fromStationId) Integer fromStationId, Param(toStationId) Integer toStationId); /** * 更新可用票数乐观锁版本 */ int updateAvailableWithVersion(TicketInventory inventory); }对应的XML文件TicketInventoryMapper.xml!-- 文件路径src/main/resources/mapper/TicketInventoryMapper.xml -- ?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.yourdomain.trainsystem.mapper.TicketInventoryMapper select idselectForUpdate resultTypeTicketInventory SELECT * FROM ticket_inventory WHERE train_id #{trainId} AND seat_type #{seatType} AND from_station_id #{fromStationId} AND to_station_id #{toStationId} FOR UPDATE !-- 关键悲观锁锁定该行记录 -- /select update idupdateAvailableWithVersion UPDATE ticket_inventory SET available #{available}, version version 1 WHERE id #{id} AND version #{version} !-- 乐观锁只有版本号匹配才更新 -- /update /mapper4.2 服务层实现订票与事务管理服务层是业务逻辑的核心。订票操作必须在一个事务内完成查询余票、扣减库存、创建订单。这里使用Spring的Transactional注解来管理事务。// 文件路径src/main/java/com/yourdomain/trainsystem/service/impl/BookingServiceImpl.java package com.yourdomain.trainsystem.service.impl; import com.yourdomain.trainsystem.entity.Order; import com.yourdomain.trainsystem.entity.TicketInventory; import com.yourdomain.trainsystem.mapper.OrderMapper; import com.yourdomain.trainsystem.mapper.TicketInventoryMapper; import com.yourdomain.trainsystem.service.BookingService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDateTime; Service RequiredArgsConstructor // Lombok注解自动注入final字段 Slf4j public class BookingServiceImpl implements BookingService { private final TicketInventoryMapper ticketInventoryMapper; private final OrderMapper orderMapper; Override Transactional(rollbackFor Exception.class) // 声明式事务任何异常都回滚 public Order bookTicket(Integer userId, Integer trainId, String seatType, Integer fromStationId, Integer toStationId) { // 1. 查询并锁定库存悲观锁 TicketInventory inventory ticketInventoryMapper.selectForUpdate(trainId, seatType, fromStationId, toStationId); if (inventory null || inventory.getAvailable() 0) { throw new RuntimeException(余票不足或车次信息不存在); } // 2. 扣减库存 inventory.setAvailable(inventory.getAvailable() - 1); int updateCount ticketInventoryMapper.updateAvailableWithVersion(inventory); if (updateCount 0) { // 乐观锁冲突更新失败在实际高并发场景可能发生 throw new RuntimeException(系统繁忙请重试); } // 3. 生成订单模拟实际应有更复杂的逻辑 Order order new Order(); order.setId(generateOrderId()); // 生成分布式订单号 order.setUserId(userId); order.setTrainId(trainId); order.setSeatType(seatType); order.setFromStationId(fromStationId); order.setToStationId(toStationId); order.setStatus(0); // 待支付 order.setAmount(calculatePrice(trainId, seatType, fromStationId, toStationId)); // 计算价格 order.setCreateTime(LocalDateTime.now()); orderMapper.insert(order); log.info(用户{}订票成功订单号{}, userId, order.getId()); return order; } private String generateOrderId() { // 简单示例实际应用应使用雪花算法等分布式ID生成器 return ORD System.currentTimeMillis() (int)(Math.random()*1000); } private BigDecimal calculatePrice(Integer trainId, String seatType, Integer fromStationId, Integer toStationId) { // 模拟价格计算实际应根据里程、座位类型等复杂计算 return new BigDecimal(150.00); } }关键点解析Transactional该方法中的所有数据库操作查询锁、更新库存、插入订单被包含在一个事务中。如果任何一步失败所有操作都会回滚保证了“要么全部成功要么全部失败”的原子性。悲观锁 (FOR UPDATE)在查询余票时使用SELECT ... FOR UPDATE这会锁定该行记录直到当前事务提交或回滚。这防止了其他事务同时修改同一条库存记录有效避免了超卖。但要注意这会降低并发性能在高并发场景下需要评估。乐观锁 (version)在更新库存时使用版本号作为条件。如果更新时发现版本号与查询时不一致说明已被其他事务修改则更新行数为0事务回滚提示用户重试。这适用于并发冲突不那么频繁的场景。4.3 控制器层暴露RESTful API控制器层负责接收HTTP请求调用服务层并返回JSON响应。这是前后端交互的桥梁。// 文件路径src/main/java/com/yourdomain/trainsystem/controller/BookingController.java package com.yourdomain.trainsystem.controller; import com.yourdomain.trainsystem.entity.Order; import com.yourdomain.trainsystem.service.BookingService; import com.yourdomain.trainsystem.vo.ApiResponse; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; RestController RequestMapping(/api/booking) RequiredArgsConstructor public class BookingController { private final BookingService bookingService; PostMapping(/book) public ApiResponseOrder bookTicket(RequestParam Integer userId, RequestParam Integer trainId, RequestParam String seatType, RequestParam Integer fromStationId, RequestParam Integer toStationId) { try { Order order bookingService.bookTicket(userId, trainId, seatType, fromStationId, toStationId); return ApiResponse.success(订票成功, order); } catch (RuntimeException e) { // 这里应该更精细地捕获不同类型的异常 return ApiResponse.fail(e.getMessage()); } } }统一的API响应封装类ApiResponse// 文件路径src/main/java/com/yourdomain/trainsystem/vo/ApiResponse.java package com.yourdomain.trainsystem.vo; import lombok.Data; Data public class ApiResponseT { private Integer code; private String message; private T data; public static T ApiResponseT success(String message, T data) { ApiResponseT response new ApiResponse(); response.setCode(200); response.setMessage(message); response.setData(data); return response; } public static T ApiResponseT fail(String message) { ApiResponseT response new ApiResponse(); response.setCode(500); response.setMessage(message); return response; } }5. 进阶实现退票与改签业务逻辑退改签是票务系统的难点涉及费用计算、库存恢复、状态流转和可能的事务补偿。5.1 退票服务实现退票不仅仅是把库存加回去。它需要考虑退票时间与发车时间的间隔计算手续费并更新订单状态。// 文件路径src/main/java/com/yourdomain/trainsystem/service/impl/RefundServiceImpl.java Service RequiredArgsConstructor Slf4j public class RefundServiceImpl implements RefundService { private final OrderMapper orderMapper; private final TicketInventoryMapper ticketInventoryMapper; Override Transactional(rollbackFor Exception.class) public boolean refundTicket(String orderId) { // 1. 查询订单并校验 Order order orderMapper.selectById(orderId); if (order null) { throw new RuntimeException(订单不存在); } if (order.getStatus() ! 1) { // 假设1是“已支付”状态 throw new RuntimeException(当前订单状态不允许退票); } // 校验退票时间规则例如发车前2小时可退 if (!canRefund(order)) { throw new RuntimeException(已超过退票时间限制); } // 2. 计算应退金额可能扣除手续费 BigDecimal refundAmount calculateRefundAmount(order); // 此处应调用支付网关进行退款操作模拟 // boolean refundSuccess paymentGateway.refund(order.getPayTransactionId(), refundAmount); boolean refundSuccess true; // 模拟退款成功 if (refundSuccess) { // 3. 恢复库存同样需要加锁或乐观锁控制 TicketInventory inventory ticketInventoryMapper.selectForUpdate( order.getTrainId(), order.getSeatType(), order.getFromStationId(), order.getToStationId()); if (inventory ! null) { inventory.setAvailable(inventory.getAvailable() 1); ticketInventoryMapper.updateAvailableWithVersion(inventory); } // 4. 更新订单状态 order.setStatus(4); // 已退票 order.setRefundAmount(refundAmount); order.setRefundTime(LocalDateTime.now()); orderMapper.updateById(order); log.info(订单{}退票成功退款金额{}, orderId, refundAmount); return true; } else { throw new RuntimeException(退款失败请稍后重试); } } private boolean canRefund(Order order) { // 简化逻辑查询车次发车时间判断当前时间是否在允许退票范围内 // 实际应从数据库查询train的departure_time LocalDateTime departureTime LocalDateTime.of(2024, 6, 1, 14, 30); // 模拟 LocalDateTime now LocalDateTime.now(); return now.isBefore(departureTime.minusHours(2)); // 发车前2小时可退 } private BigDecimal calculateRefundAmount(Order order) { // 简化逻辑根据退票时间与发车时间的间隔计算手续费比例 // 实际业务规则更复杂 BigDecimal originalAmount order.getAmount(); // 假设扣除10%手续费 return originalAmount.multiply(new BigDecimal(0.9)); } }5.2 改签服务实现改签比退票更复杂可以理解为“退旧票 订新票”的组合操作但需要在同一个事务中完成并且要处理新旧票务库存的联动。Override Transactional(rollbackFor Exception.class) public boolean changeTicket(String originalOrderId, Integer newTrainId, String newSeatType, Integer newFromStationId, Integer newToStationId) { // 1. 锁定原订单并校验 Order originalOrder orderMapper.selectByIdForUpdate(originalOrderId); // 需要实现行锁 // ... 状态、时间等校验 // 2. 锁定并查询新票库存 TicketInventory newInventory ticketInventoryMapper.selectForUpdate(newTrainId, newSeatType, newFromStationId, newToStationId); if (newInventory null || newInventory.getAvailable() 0) { throw new RuntimeException(目标车次余票不足); } // 3. 计算改签差价 BigDecimal newPrice calculatePrice(newTrainId, newSeatType, newFromStationId, newToStationId); BigDecimal priceDiff newPrice.subtract(originalOrder.getAmount()); // 处理支付/退款逻辑模拟 // 4. 核心操作恢复旧库存 扣减新库存 更新订单 // 恢复旧库存 TicketInventory oldInventory ticketInventoryMapper.selectForUpdate( originalOrder.getTrainId(), originalOrder.getSeatType(), originalOrder.getFromStationId(), originalOrder.getToStationId()); oldInventory.setAvailable(oldInventory.getAvailable() 1); ticketInventoryMapper.updateAvailableWithVersion(oldInventory); // 扣减新库存 newInventory.setAvailable(newInventory.getAvailable() - 1); ticketInventoryMapper.updateAvailableWithVersion(newInventory); // 更新原订单为“已改签”状态或创建一条新订单记录根据业务设计 originalOrder.setStatus(5); // 已改签 // ... 更新其他字段如新车次信息、差价等 orderMapper.updateById(originalOrder); log.info(订单{}改签成功新车次{}, originalOrderId, newTrainId); return true; }关键点改签操作涉及对多个数据库记录两个库存记录一个订单记录的更新必须放在同一个事务中并使用行锁 (SELECT ... FOR UPDATE) 来防止并发修改导致的数据不一致。6. 系统运行与效果验证完成核心代码编写后我们需要让系统跑起来并进行功能测试。6.1 启动Spring Boot应用在IDEA中直接运行主类TrainsystemApplication名称可能不同的main方法。或在项目根目录下使用Maven命令启动mvn spring-boot:run看到控制台输出类似以下的日志说明启动成功Started TrainsystemApplication in 5.123 seconds (JVM running for 5.789)6.2 使用Postman或CURL测试API我们以订票接口为例进行测试。1. 准备测试数据确保数据库中有测试车次、区间和库存。例如向ticket_inventory表插入一条记录INSERT INTO ticket_inventory (train_id, seat_type, from_station_id, to_station_id, total, available, version) VALUES (1, SECOND_CLASS, 1001, 1002, 100, 50, 0);2. 发送HTTP POST请求使用Postman创建一个POST请求URL:http://localhost:8080/api/booking/bookBody:x-www-form-urlencodedParameters:userId: 1trainId: 1seatType: SECOND_CLASSfromStationId: 1001toStationId: 10023. 预期响应如果成功应返回类似以下的JSON{ code: 200, message: 订票成功, data: { id: ORD1717246800123456, userId: 1, trainId: 1, seatType: SECOND_CLASS, fromStationId: 1001, toStationId: 1002, status: 0, amount: 150.00, createTime: 2024-06-01T10:30:00 } }同时检查数据库ticket_inventory表中对应记录的available字段应变为 49。order表中应新增一条订单记录。6.3 并发测试模拟使用Jmeter或简单脚本为了验证锁机制是否有效可以模拟并发订票。编写一个简单的Java多线程测试程序或者使用JMeter工具在短时间内如1秒内向同一个车次、座位、区间发送多个订票请求。预期结果如果使用悲观锁 (FOR UPDATE)请求会被数据库串行化处理。最终售出的票数不会超过库存总数数据库available字段不会变为负数。如果使用乐观锁在高并发下部分请求会因版本号冲突而失败返回“系统繁忙请重试”但数据一致性依然能得到保证。7. 常见问题与排查思路在开发和部署过程中你可能会遇到以下典型问题。问题现象可能原因排查方式解决方案启动报错Failed to configure a DataSource数据库连接配置错误或数据库服务未启动。1. 检查application.yml中的url,username,password。2. 检查MySQL服务是否运行 (netstat -an | grep 3306)。3. 检查数据库名train_ticket_db是否存在。1. 修正配置。2. 启动MySQL服务。3. 创建数据库。启动报错java.lang.ClassNotFoundException: com.mysql.cj.jdbc.DriverMySQL驱动依赖未正确引入或版本不匹配。检查pom.xml中mysql-connector-java的依赖确认版本如8.0.33。在pom.xml中添加或更新驱动依赖然后执行mvn clean install。MyBatis映射文件找不到mapper-locations配置路径错误或XML文件未被Maven打包。1. 检查application.yml中mybatis.mapper-locations的值。2. 检查target/classes目录下是否有对应的.xml文件。1. 确保路径正确如classpath:mapper/*.xml。2. 在pom.xml的build部分添加资源包含配置。事务不生效数据未回滚1. 异常未被抛出或被捕获。2. 方法不是public。3. 异常类型不是RuntimeException或Error。1. 检查方法是否被try-catch且未在catch中抛出。2. 检查方法访问修饰符。3. 检查Transactional(rollbackForException.class)是否配置。1. 确保异常能传播到事务管理器。2. 将方法设为public。3. 在Transactional中指定rollbackFor。订票时出现超卖库存为负未正确使用锁机制。在高并发下多个线程同时读到相同的available值。检查订票服务的数据库操作是否在事务内并且查询库存时是否使用了FOR UPDATE或乐观锁。按照本文4.2节实现悲观锁或乐观锁。使用FOR UPDATE导致性能瓶颈或死锁锁范围过大或事务时间过长。1. 检查SQL索引是否有效idx_train_route。2. 检查事务内是否进行了耗时操作如远程调用。3. 使用数据库监控工具查看锁等待。1. 确保查询条件有索引。2. 尽量缩短事务时间将非数据库操作移出事务。3. 考虑使用乐观锁或分布式锁。前端请求报404错误1. 控制器请求路径 (RequestMapping) 错误。2. 请求方法 (GET/POST) 不匹配。3. Spring Boot未扫描到控制器类。1. 检查浏览器/Postman的请求URL和控制器路径。2. 检查控制器方法上的PostMapping或GetMapping。3. 检查主启动类是否在控制器类的上级包中。1. 修正URL或注解路径。2. 确保主类有SpringBootApplication且包路径正确。8. 项目优化与最佳实践建议一个能通过答辩的毕设不仅要能运行还要体现出你的工程化思考。以下是一些优化方向和实践建议。8.1 数据库与性能优化索引优化在ticket_inventory表的(train_id, seat_type, from_station_id, to_station_id)上建立复合索引能极大提升查询和加锁效率。读写分离对于查询余票等读多写少的操作可以考虑使用MySQL主从复制将读请求路由到从库减轻主库压力。连接池配置在application.yml中配置HikariCP等高性能连接池参数如最大连接数、最小空闲连接等。spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 300008.2 业务逻辑与代码质量参数校验在Controller层或Service层入口使用JSR 303 Bean Validation如NotNull,Min或自定义校验器对输入参数进行校验避免无效数据进入核心逻辑。统一异常处理使用Spring的ControllerAdvice和ExceptionHandler创建全局异常处理器将不同的异常如业务异常、参数异常、系统异常转换为友好的API错误响应而不是直接抛出RuntimeException。日志规范使用SLF4J Logback在关键业务节点如订单创建成功、库存更新、支付回调记录INFO日志在异常处记录ERROR日志并带上上下文信息便于线上排查。接口幂等性对于创建订单、支付等关键接口通过订单号、流水号等唯一标识实现幂等防止网络重试导致重复下单。8.3 高并发场景下的进阶思考答辩亮点库存预扣与最终扣减在真正支付成功前可以先“预扣”库存将可用票数减少并记录一个“预扣”状态支付成功后再转为“已售”。支付超时则释放预扣库存。这能更好地应对秒杀场景。分布式锁如果系统未来要部署为多节点集群数据库行锁就无法跨JVM生效了。此时需要考虑使用Redis分布式锁如Redisson或ZooKeeper来保证集群环境下的库存一致性。异步与消息队列将非核心、耗时的操作如发送出票短信、生成电子发票异步化通过消息队列如RocketMQ, RabbitMQ解耦提升主流程的响应速度。缓存应用车次信息、站点信息等变动不频繁的数据可以放入Redis缓存减少数据库查询压力。8.4 安全注意事项SQL注入MyBatis使用#{}占位符可以有效防止SQL注入严禁在XML中拼接SQL语句如LIKE %${name}%。权限控制确保用户只能操作自己的订单。在Controller方法中必须校验当前登录用户ID与订单所属用户ID是否匹配。敏感数据脱敏日志中不应打印完整的用户身份证号、手机号等敏感信息。生产环境配置数据库密码、第三方API密钥等敏感信息不应硬编码在application.yml中应使用环境变量或配置中心如Apollo管理。9. 总结与项目扩展方向通过本文的拆解你应该已经掌握了基于SSM框架实现一个火车票预售系统的核心脉络。我们从为什么选择这个项目开始梳理了业务概念和架构然后一步步实现了环境搭建、数据库设计、核心订票业务包含悲观锁/乐观锁、退改签复杂逻辑并给出了测试验证和问题排查的方法。这个项目作为毕业设计已经具备了完整的功能和一定的技术深度。如果你希望让它更出彩可以考虑以下扩展方向引入Spring Security实现完整的用户认证与授权区分用户和管理员角色。增加支付模块模拟集成支付宝或微信支付的沙箱环境模拟完整的支付流程。实现简单的后台管理页面使用Thymeleaf或Vue.js Element UI为管理员提供车次管理、订单查询等功能。编写单元测试与集成测试使用JUnit和Spring Boot Test对Service层的关键方法进行测试保证代码质量。容器化部署编写Dockerfile将应用打包成Docker镜像并尝试使用Docker Compose一键启动整个系统包含MySQL、Redis等。最后建议你在GitHub或Gitee上为这个项目创建一个代码仓库使用规范的Commit信息并撰写清晰的README文档。这不仅能帮助你进行版本管理也能成为你个人技术能力的一个有力证明。希望这个项目能成为你Java学习之路上的一个扎实的里程碑。
返回列表