首页/新闻资讯/正文详情

3天搞定广州市公路客运网上售票系统:告别报错,性能优化实战

发布时间:2026/9/22 22:15:16 来源:云帆数科 栏目:资讯中心
3天搞定广州市公路客运网上售票系统:告别报错,性能优化实战
3天搞定广州市公路客运网上售票系统:告别报错,性能优化实战 盯着屏幕上那一片刺眼的红色 Stack Trace,你大概率已经想砸键盘了。NullPointerException 或者 ConnectionTimeout 满屏飞,根本不知道从哪一行代码开始查起。这种“报错一堆看不懂”的绝望感,是每个后端开发在接手或重构老系统时的必经之路。 今天我们要聊的,是一个极具代表性的实战案例:广州市公路客运网上售票系统。别被这个名字吓到,它本质上就是一个高并发的分布式票务系统。我见过太多开发者在这个系统里栽跟头,不是代码逻辑错了,而是性能优化没做对,导致高峰期直接崩盘。 这篇文章不整虚的,我们直接从工程化角度,把这个系统从零搭起来。你会看到真实的目录结构、核心代码实现,以及那些能让系统扛住并发洪流的性能优化技巧。哪怕你是刚入行的新手,跟着敲完这套代码,对高并发处理的理解也能上一个台阶。 项目目标与架构选型 在动手写代码之前,先明确我们要解决什么问题。一个标准的公路客运售票系统,核心业务流程很简单:用户查询班次 - 选择座位 - 下单支付 - 出票。但魔鬼藏在细节里:高并发查询:春运期间,成千上万用户同时查询同一线路,数据库扛不住。 座位超卖:两个用户同时抢最后一个座位,必须保证数据一致性。 库存扣减:票务库存是核心资源,必须原子操作。为了在 3 天内跑通核心流程并具备可扩展性,我们选择 Spring Boot + MyBatis-Plus + Redis + MySQL 技术栈。这是国内企业最主流的组合,招聘需求量大,且资料丰富。 为什么选 Redis? 因为 MySQL 在处理高频读请求时,性能瓶颈非常明显。我们将班次信息、剩余票数等热点数据缓存到 Redis 中,数据库只负责最终持久化和复杂查询。 核心目标:实现座位的并发安全锁定。 查询接口响应时间控制在 50ms 以内。 支持水平扩展,无状态服务设计。目录结构与工程化规范 很多初学者喜欢把代码全堆在 Service 层,导致后期维护简直是灾难。我们采用标准的分层架构,确保代码职责单一。 ticket-system/ ├── src/main/java/com/gz/ticket/ │ ├── config/ # 配置类(Redis, Web, Exception) │ ├── controller/ # 接口层(接收请求,参数校验) │ ├── service/ # 业务层(核心逻辑,事务控制) │ ├── mapper/ # 数据层(MyBatis 接口) │ ├── entity/ # 数据库实体类 │ ├── dto/ # 数据传输对象(前端交互) │ ├── common/ # 通用工具(Result, 常量, 异常) │ └── TicketApplication.java ├── src/main/resources/ │ ├── application.yml # 配置文件 │ ├── mapper/ # MyBatis XML 映射文件 │ └── static/ # 静态资源(可选) └── pom.xml关键文件说明:Result.java:统一响应格式。前端讨厌不一致的返回结构,我们定义 {code, msg, data} 三元组,所有接口必须返回这个对象。 GlobalExceptionHandler.java:全局异常捕获。这是解决“报错看不懂”的第一道防线。所有的 Exception 都会在这里被拦截,转换成友好的 JSON 返回给前端,而不是直接抛出 500 页面。在 pom.xml 中,除了引入 Spring Boot Starter Web 和 MyBatis Plus,别忘了引入 lombok 和 hutool。Lombok 能减少大量的 Getter/Setter 样板代码,Hutool 提供了一些便捷的字符串和集合工具,能提升开发效率。 核心代码实现与逐行解析 接下来进入硬核部分。我们实现最核心的功能:查询班次列表 和 锁定座位。 1. 统一异常处理:让报错不再神秘 很多开发者遇到报错,第一反应是看控制台,但生产环境控制台往往只有一行 Internal Server Error。我们需要一个全局异常处理器。 import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import com.gz.ticket.common.Result; import lombok.extern.slf4j.Slf4j;@Slf4j @RestControllerAdvice public class GlobalExceptionHandler {/*** 捕获所有未处理的异常*/@ExceptionHandler(Exception.class)public Result? handleException(Exception e) {// 关键:记录完整堆栈到日志文件,而不是直接返回给前端log.error(系统发生未知异常, e);// 返回给前端的错误信息要模糊,避免暴露系统细节return Result.error(500, 系统繁忙,请稍后再试);}/*** 捕获业务自定义异常*/@ExceptionHandler(BusinessException.class)public Result? handleBusinessException(BusinessException e) {// 业务异常,比如“座位已售罄”,直接返回具体错误码return Result.error(e.getCode(), e.getMessage());} }逐行讲解:@RestControllerAdvice:这是 Spring 提供的注解,类似于 @ControllerAdvice,但返回的是 JSON 而非视图。它会将所有 Controller 层抛出的异常集中处理。 log.error(..., e):注意这里传入了异常对象 e。Slf4j 会自动打印完整的 StackTrace 到日志文件。这样即使前端只看到“系统繁忙”,运维人员也能在日志里看到确切的报错行号。 避坑指南:千万不要在异常处理器里 e.printStackTrace(),这只会输出到标准输出流,生产环境根本看不到。必须使用日志框架。2. 班次查询:Redis 缓存优化 查询接口是流量最大的入口。如果每次都查 MySQL,数据库连接池很快会被打满。 @Service public class TicketService {@Autowiredprivate TicketMapper ticketMapper;@Autowiredprivate RedisTemplateString, Object redisTemplate;private static final String CACHE_KEY_PREFIX = ticket:line:;/*** 查询某条线路的班次列表*/public ListTicketDto queryLine(String lineId) {String cacheKey = CACHE_KEY_PREFIX + lineId;// 1. 尝试从 Redis 获取ListTicketDto cacheData = (ListTicketDto) redisTemplate.opsForValue().get(cacheKey);if (cacheData != null) {return cacheData;}// 2. 缓存未命中,查询数据库ListTicket tickets = ticketMapper.selectByLineId(lineId);// 3. 转换为 DTO 并放入 RedisListTicketDto dtoList = tickets.stream().map(this::convertToDto).collect(Collectors.toList());// 设置过期时间 5 分钟,防止数据不一致redisTemplate.opsForValue().set(cacheKey, dtoList, 5, TimeUnit.MINUTES);return dtoList;} }性能优化要点:缓存穿透防护:如果查询的数据在 DB 中也不存在,cacheData 为 null,我们会去查 DB。如果 DB 也没数据,我们应该缓存一个空对象或特殊标记,防止恶意请求频繁击穿缓存。 TTL 设置:5 分钟是一个经验值。太短了缓存命中率低,太长了用户看到的座位状态可能滞后。根据业务场景调整。3. 座位锁定:解决并发超卖 这是最容易出 Bug 的地方。如果两个用户同时点击“下单”,普通的 UPDATE 语句会导致超卖。 错误示范(绝对不要用): // 1. 查库存 int count = ticketMapper.selectCount(stockId); // 2. 判断库存 if (count 0) {// 3. 扣减库存ticketMapper.updateStock(stockId, -1); }在并发下,步骤 1 和 3 之间有时间差,两个线程可能同时读到 count 0,导致多卖一张票。 正确方案:数据库乐观锁 + Redis 预扣减 为了简化演示,我们采用数据库层面的原子操作。 /*** 锁定座位* @param ticketId 班次ID* @param seatNo 座位号*/ public void lockSeat(Long ticketId, String seatNo) {// 1. 构造更新条件// 利用数据库的 UPDATE ... WHERE status = 0 (空闲)// 如果 status 已经是 1 (已锁定),则更新行数为 0int rows = ticketMapper.lockSeat(ticketId, seatNo);// 2. 判断更新结果if (rows == 0) {throw new BusinessException(400, 座位已被占用或不存在);}// 3. 发送延迟队列消息,如果 15 分钟未支付,释放座位// 这里省略 RabbitMQ/RocketMQ 的具体实现,仅示意逻辑sendReleaseMessage(ticketId, seatNo, 15); }Mapper XML 中的 SQL: update id=lockSeatUPDATE t_ticket_seat SET status = 1, lock_time = NOW(),lock_user_id = #{userId}WHERE ticket_id = #{ticketId} AND seat_no = #{seatNo} AND status = 0 /update原理解析:原子性:UPDATE 语句在数据库层面是原子操作。WHERE status = 0 是关键。如果第一个线程已经把 status 改成了 1,第二个线程执行同样的 SQL,影响行数(rows)就是 0。 无锁并发:这种方案利用了 MySQL 的行锁机制(InnoDB 引擎默认支持),不需要我们在代码里加 synchronized 或 ReentrantLock。对于高并发场景,数据库的行锁比应用层的 JVM 锁更可靠,因为它不依赖单机。运行与测试:如何验证性能 代码写完只是第一步,必须通过测试来验证性能优化是否生效。 1. 本地运行 确保 MySQL 和 Redis 服务已启动。修改 application.yml 中的连接配置: spring:datasource:url: jdbc:mysql://localhost:3306/gz_ticket?useUnicode=truecharacterEncoding=utf8username: rootpassword: 123456redis:host: localhostport: 6379启动项目,访问 /actuator/health 确认服务正常。 2. 压力测试 使用 JMeter 或 Locust 模拟并发请求。 测试场景:并发用户数:1000 个用户。 操作:同时查询同一热门线路(如广州-深圳),并尝试购买相同的 10 个座位。 预期结果:查询接口 QPS(每秒查询率)应高于 5000。 座位购买成功率:10 个座位,最终只有 10 个订单成功,其余 990 个请求应返回“座位已被占用”。 绝对不能出现:11 个订单成功(超卖)。常见测试报错: 如果在测试中出现 Connection Pool Exhausted(连接池耗尽),说明数据库连接数不够。调整 HikariCP 配置: spring:datasource:hikari:maximum-pool-size: 50 # 根据 CPU 核心数调整,通常为 2 * CPU核数 + 磁盘数优化扩展与避坑指南 系统跑通后,还有几个关键点决定它能走多远。 1. 缓存一致性 Redis 和 MySQL 数据不一致是常态。策略:采用“Cache Aside Pattern”(旁路缓存)。更新数据库后,删除缓存,而不是更新缓存。 原因:更新缓存可能失败,且并发更新时容易乱序。删除缓存后,下次请求会重新加载最新数据。2. 数据库索引优化 在 t_ticket_seat 表上,必须建立联合索引: CREATE INDEX idx_ticket_seat_status ON t_ticket_seat(ticket_id, seat_no, status);这个索引覆盖了查询和更新操作,避免了全表扫描。在高并发下,索引缺失是性能杀手。 3. 日志规范化 不要到处打 System.out.println。统一使用 @Slf4j。INFO 级别:记录关键业务流程(如下单成功、支付成功)。 DEBUG 级别:记录调试信息(如 SQL 执行时间),生产环境关闭。 ERROR 级别:记录异常,必须包含堆栈信息。4. 参考开源项目 如果你想看更复杂的分布式锁实现(如 Redisson),可以参考 GitHub 上的开源仓库 Redisson。它在 distributed_lock 模块中实现了 Redlock 算法,比简单的 SETNX 更安全可靠。阅读源码能帮你理解底层原理。 小结 从报错一堆看不懂,到能够从容应对高并发场景,关键在于工程化思维和对细节的掌控。全局异常处理让你不再迷失在 StackTrace 里。 Redis 缓存让查询接口轻快如风。 数据库原子操作保证了数据的一致性,杜绝超卖。 压力测试是检验优化效果的唯一标准。搭建一个广州市公路客运网上售票系统,不仅是写代码,更是学习如何设计一个健壮、可扩展的后端架构。这些技术点(缓存、锁、索引、日志)在任何电商、票务、金融系统中都是通用的。 你在项目里踩过这个坑吗?比如缓存穿透导致数据库宕机,或者并发下数据不一致?评论区聊聊,我们一起拆解解决方案。

相关推荐

一文搞懂如何在电脑上安装打印机:从驱动握手到数据流
一文搞懂如何在电脑上安装打印机:从驱动握手到数据流

一文搞懂如何在电脑上安装打印机:从驱动握手到数据流 学会语法却不知怎么搭项目,这是无数技术人的通病。你背熟了TCP/IP模型,却在本地网络里卡住,连个打印机都配不通。其实, 如何在电脑上安装打印机… · 2026/9/22 22:15:03

《WiFi 嵌入式物联网开发全套实战》| 第 16 章 ESP32 AP+STA 双模共存原理与工程坑点
《WiFi 嵌入式物联网开发全套实战》| 第 16 章 ESP32 AP+STA 双模共存原理与工程坑点

专栏:《WiFi 嵌入式物联网开发全套实战》 专栏定位:嵌入式 Linux/ESP32 WiFi 从原理→驱动→配网→协议→稳定性→抓包调试→量产优化全套工业实战 适配:物联网设备、智能家居、工控网关、无线透传设备、4GWiFi 双模设备 💖 点赞 … · 2026/9/22 22:14:51

ENFP型人格算法解析:一文搞懂核心逻辑与实战避坑
ENFP型人格算法解析:一文搞懂核心逻辑与实战避坑

ENFP型人格算法解析:一文搞懂核心逻辑与实战避坑 版本升级后 API 全变了,是不是让你瞬间头皮发麻?别急,这种“面目全非”的焦虑在编程圈太常见了。今天咱们不聊虚的,直接拆解【enfp型人格】这个看似玄学实则极具工程价值的概念。很多人以为… · 2026/9/22 22:14:44

2026最新 sta手写实现 面试必过指南
2026最新 sta手写实现 面试必过指南

2026最新 sta手写实现 面试必过指南 官方文档翻了三遍还是云里雾里?别慌,这种“看起来简单,写起来就崩”的底层机制,正是大厂面试最爱挖坑的地方。 在2026最新的后端面试标准里, sta (状态机/状态转换逻辑)不再是简单的… · 2026/9/22 23:54:42

k222性能优化实战:3个完整示例教你把响应时间砍半
k222性能优化实战:3个完整示例教你把响应时间砍半

k222性能优化实战:3个完整示例教你把响应时间砍半 看了一堆教程还是不会写项目?别急着怀疑自己,90%的新手卡壳不是因为笨,而是没人给过你一份能直接跑通的 完整示例… · 2026/9/22 23:54:29

六顶思考帽避坑指南:5个步骤解决代码跑不通
六顶思考帽避坑指南:5个步骤解决代码跑不通

六顶思考帽避坑指南:5个步骤解决代码跑不通 复制来的代码跑不通,你是不是也经历过那种“明明照着教程敲,结果报错一堆”的崩溃时刻?很多开发者在 CSDN… · 2026/9/22 23:54:23

前端避坑指南:彻底搞懂 hao123.com.com 域名解析与请求陷阱
前端避坑指南:彻底搞懂 hao123.com.com 域名解析与请求陷阱

前端避坑指南:彻底搞懂 hao123.com.com 域名解析与请求陷阱 官方文档太长抓不住重点,导致很多开发者在对接第三方服务或处理特定域名逻辑时,总踩重复的坑。今天这篇避坑指南,专门拆解 hao123.com.com… · 2026/9/22 23:54:16

3天搞定撅嘴表情包:从入门到精通的面试通关秘籍
3天搞定撅嘴表情包:从入门到精通的面试通关秘籍

3天搞定撅嘴表情包:从入门到精通的面试通关秘籍 你是不是也这样?网上搜“撅嘴表情包”,出来一堆静态图,想做成动态效果或者在App里集成,看了一堆教程还是不会写项目。别急,今天这篇不聊虚的,直接拆解大厂面试中关于这类视觉交互资源的高频考点。… · 2026/9/22 23:54:16

免费试听歌曲加载慢?3个技巧解决版本升级API痛点
免费试听歌曲加载慢?3个技巧解决版本升级API痛点

免费试听歌曲加载慢?3个技巧解决版本升级API痛点 刚把音乐播放器的核心模块从旧版 API 切换到新版,结果一跑测试,CPU 占用率直接飙红,首屏加载时间从 200ms 暴涨到 2.5s。这不仅是我的噩梦,也是无数开发者在应对… · 2026/9/22 23:54:10

5个电影海报图片处理坑,新手避坑指南
5个电影海报图片处理坑,新手避坑指南

5个电影海报图片处理坑,新手避坑指南 刚写完代码,一运行屏幕直接炸了。满屏红色的 StackTrace 滚得比弹幕还快,什么 NullPointerException 、 ImageIO.read() returned null 、… · 2026/9/22 0:00:07

注册微信公众账号:一文搞懂从0到1全流程
注册微信公众账号:一文搞懂从0到1全流程

注册微信公众账号:一文搞懂从0到1全流程 复制来的代码跑不通,报错信息满屏飞,到底卡在哪?别急,咱们先停下手里的调试。很多开发者觉得注册微信公众账号只是填个表单、传个身份证那么简单,真上手才发现坑深不见底。今天这篇 一文搞懂… · 2026/9/22 0:00:07

手写实现图片压缩网站核心:搞定WebP转换与质量调优
手写实现图片压缩网站核心:搞定WebP转换与质量调优

手写实现图片压缩网站核心:搞定WebP转换与质量调优 复制来的代码跑不通不知道怎么调?别慌,这种“复制粘贴地狱”在开发圈太常见了。尤其是做 图片压缩网站… · 2026/9/22 0:00:19

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码