ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot高并发抢票系统:Redis防超卖与RabbitMQ异步落库实战

SpringBoot高并发抢票系统:Redis防超卖与RabbitMQ异步落库实战 1. 项目概述与核心痛点拆解这个项目是我上个月帮一个做演出票务的朋友做的演唱会售票管理系统从需求对接到部署上线差不多用了三周时间。系统的核心目标很明确支持演唱会场次管理、座位分区定价、用户抢票下单、订单支付与退票以及后台的销售数据统计。技术栈采用 SpringBoot 作为后端主框架前端用 Vue 做管理后台和用户端页面数据库选了 MySQL缓存和分布式锁用 Redis消息队列用了 RabbitMQ。为什么我会选择这套方案而不是直接用现成的低代码平台或者纯 PHP 写一套关键在于演唱会售票这个场景有几个非常典型的业务特点一是短时间内通常是开票后几十秒到几分钟会涌入大量并发请求比如一场 5000 人的场馆开票瞬间可能同时有数万人点击抢票二是票务数据有强一致性要求座位不能超卖、同一个座位不能同时卖给两个人三是订单状态流转复杂涉及锁定座位、生成订单、支付回调、超时释放、退票回补等多个环节。SpringBoot 生态天然适合做这类业务系统的快速开发加上 Redis 和 MQ 的组合可以很好地解决高并发和异步解耦的问题。适合看这篇文章的人有两类。一类是准备用 SpringBoot 做实战项目、想搞懂高并发下到底怎么防超卖的学生或初级开发另一类是真正要接票务、演出、峰会等有抢票环节业务的开发者想从别人的架构设计里拿一套能落地的方案。如果你只是想做一套 CRUD 的管理系统那你可以关掉这篇了因为这篇文章重点不是教你怎么写增删改查而是解决业务高峰期系统会不会挂、会不会卖重票这两个核心问题。先说整体架构。系统拆成了用户端、管理端两个前端工程接口层放在同一个 SpringBoot 应用里按模块分包。抢票链路单独抽了一个服务接口内部流程是请求进来先做前置校验登录、限购规则、场次状态然后通过 Lua 脚本做 Redis 库存预扣减扣减成功后再发消息给 RabbitMQ由消费者异步去 MySQL 生成订单和更新座位状态。为什么不直接在 MySQL 里扣库存因为数据库行锁在高并发下会迅速成为瓶颈出现大量线程阻塞和连接池耗尽实测在 5000 并发下 MySQL 方案接口响应时间会飙升到 10 秒以上而 Redis 内存操作能在毫秒级完成。MySQL 只作为最终数据落盘的角色配合对账任务保证 Redis 和数据库的数据一致。1.1 业务链路闭环设计我花了一整天和票务方的业务人员聊流程最后把整个系统的业务链路梳理成这么一条线用户浏览演出列表和场次详情 → 选择票价区和座位 → 提交订单并锁定座位 → 进入支付流程 → 支付成功同步出票 → 未支付超时释放座位。访问控制上还要支持退票、改签和后台的手动锁座、赠票。很多第一次做票务系统的人容易漏掉两个细节。第一个是同一个用户对同一场演唱会的限购规则如果只能买 4 张那抢票接口必须要做幂等和计数控制否则黄牛直接写脚本刷接口就能无限囤票。第二个是座位状态的变化不能只有空和已卖两个状态必须引入锁定中这个中间态。用户选了座位开始下单这个座位要先锁住不然两个用户同时看中同一个座位A 先下单但还没支付B 后下单却直接把票付了就会产生超卖投诉。我给出的方案是座位状态机可用0→ 锁定1→ 已售2其中锁定状态会携带一个过期时间到期未支付自动回滚到可用。1.2 并发估量与技术选型思路初步压测是我们自己搭的模拟环境用 JMeter 模拟 3000 个并发用户同时抢 500 张票持续 30 秒。这个量级不算极端但已经能暴露大部分架构问题。压测结果让我确认了几个选型判断第一数据库连接池必须控制好上限。最开始用默认的 HikariCP 配置maximumPoolSize10压测一上来连接池直接耗尽大量请求在等待获取连接错误率超过 40%。后来调到 50同时配合 Redis 做前置拦截真正打到数据库的写请求其实只有成功锁定座位的那些用户大大减轻了压力。第二RabbitMQ 的消费者数量要跟数据库写入能力匹配。消费者太少消息积压用户明明抢到票了却迟迟收不到订单确认消费者太多数据库写入并发过高行锁等待严重反而拖慢整体速度。我最终把消费者并发数设置为数据库连接池的一半并开了手动 ACK确保消息处理的可靠性和吞吐量之间的平衡。第三直接用普通同步接口做抢票是行不通的。一开始图省事接口逻辑就是查库存 → 减库存 → 生成订单上线前压测直接被打爆。改成请求先进 Redis 扣减再返回排队成功异步生成订单之后接口响应时间降到了 200ms 以内体验和可靠性都提升了一个档次。2. 数据库模型设计与核心表结构系统的数据模型在设计阶段就要考虑业务的可扩展性所以我把表结构拆得相对细一些。核心表一共有六张用户表、演唱会表、场次表一个演唱会可能有多场、座位表、订单表、支付流水表。另外还有几张辅助表比如限购规则表、退票记录表这里不展开。我先贴出四张核心表的建表 SQL这是我经过多轮调整后的最终版本每一步注释都标注了设计原因。-- 演唱会场次表 CREATE TABLE concert_session ( id bigint(20) NOT NULL AUTO_INCREMENT, concert_name varchar(128) NOT NULL COMMENT 演唱会名称, venue varchar(128) NOT NULL COMMENT 场馆名称, start_time datetime NOT NULL COMMENT 开演时间, sale_start_time datetime NOT NULL COMMENT 开票时间, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 场次状态 0未开始 1售票中 2已售罄 3已结束, remark varchar(255) DEFAULT NULL, PRIMARY KEY (id), KEY idx_start_time (start_time), KEY idx_status (status) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT演唱会场次表;这张表里有两个时间字段特别关键开演时间和开票时间。很多系统在做自动上下架、自动关闭订单时都要依赖这两个时间。比如开演前 2 小时强制关闭未支付订单、开票时间未到不允许抢票这些业务规则我都建议直接放在数据库字段里而不是写死在后端代码中方便运营随时调整。状态字段我预留了位点没有用布尔值因为场次生命周期不只是卖完/没卖完两种还要覆盖未开始、售票中、已售罄、已结束四个阶段。-- 座位表 CREATE TABLE seat ( id bigint(20) NOT NULL AUTO_INCREMENT, session_id bigint(20) NOT NULL COMMENT 场次ID, section varchar(32) NOT NULL COMMENT 区域如A区/B区, row_no varchar(16) NOT NULL COMMENT 排号, col_no varchar(16) NOT NULL COMMENT 座号, price decimal(10,2) NOT NULL COMMENT 价格该价位, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态 0可用 1锁定 2已售, lock_expire_time datetime DEFAULT NULL COMMENT 锁定过期时间, version int(11) NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, PRIMARY KEY (id), UNIQUE KEY uk_session_seat (session_id, section, row_no, col_no), KEY idx_status (status), KEY idx_lock_expire (lock_expire_time) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT座位表;座位表是全文最核心的表。设计时我做了两个不少系统没做的事一是加了唯一索引uk_session_seat从数据库层面保证同一个场次同一个座位只能有一条记录防止程序逻辑漏洞导致的数据重复二是加了lock_expire_time字段配合定时任务把超时未支付的锁定座位释放回可用状态。另外version字段是为了乐观锁预留的虽然实际扣库存走了 Redis 预扣减但座位状态的最终更新仍需要对账补偿乐观锁双保险。2.1 一张订单表如何记录多张票订单表的结构我犹豫过很久是一个订单关联多个座位还是一个座位一个订单考虑到用户一次可能买 2 到 4 张连座票最终选择了主订单 订单明细的方式。主订单表存总金额、状态、用户 ID、场次 ID明细表存每个座位的编号和单座价格。-- 订单主表 CREATE TABLE order_info ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(64) NOT NULL COMMENT 订单编号, user_id bigint(20) NOT NULL COMMENT 用户ID, session_id bigint(20) NOT NULL COMMENT 场次ID, total_amount decimal(10,2) NOT NULL COMMENT 订单总额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态 0待支付 1已支付 2已取消 3已退票 4已关闭, pay_expire_time datetime NOT NULL COMMENT 支付过期时间, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_session_status (session_id, status) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT订单主表;订单编号我用了日期 随机数 用户尾号的组合比如20250620150000123456。这个编号有业务含义出现问题时运营可以直接根据订单号判断是哪天的单子。注意订单状态里有一个已关闭和已取消的区分——已取消是用户主动取消已关闭是系统超时自动关闭对账时需要对这两种状态区分处理。2.2 数据一致性与对账策略Redis 缓存了库存和座位状态MySQL 表记录的是最终数据两层之间存在短暂的不一致窗口期。如果消费者异步落库失败Redis 已经扣减了库存但数据库没有生成订单就会出现票少了但没人买走的问题。我的做法是引入一个定时对账任务每隔 5 分钟扫描一遍 Redis 中的扣减流水和 MySQL 订单表把没有对应订单的扣减记录找出来回补 Redis 库存同时记录日志方便排查。再一个容易被忽略的点是订单超时未支付后的座位释放。用户锁定座位后如果不支付后台有两种释放策略一是用户主动取消订单时立刻释放二是支付过期时间到了之后由定时任务批量扫描待支付且已过期的订单批量释放座位并更新订单状态。这里要注意释放座位不是简单地update seat set status 0而是要先确认该座位确实还是锁定状态且锁定人就是该订单用户防止释放了别的用户的座位。3. 高并发抢票核心链路实现重头戏来了。如果前面的表结构设计和多数据源方案属于地基那这一部分就是决定系统能不能抗住流量的承重墙。我在这个项目里的抢票链路一共分五步前置校验 → Redis 预扣减 → 发送 MQ → 异步落库 → 对账补偿。下面把每一部分的关键代码和配置都贴出来并解释为什么这么做。3.1 五分钟学会的 Redis Lua 扣减脚本前端请求进入抢票接口后首先经过前置校验用户是否登录、场次是否存在、当前是否在售票时间内、该用户是否已超过限购数量。这些校验全部通过后直接进入 Redis 预扣减。这里我用了一段 Lua 脚本目的是保证判断库存和扣减库存两个操作是原子性的避免并发下库存扣成负数。-- KEYS[1] 场次库存key例如 ticket:stock:1001 -- KEYS[2] 用户限购key例如 ticket:user:1001:888 -- ARGV[1] 购买数量 -- ARGV[2] 用户限购上限 local stock tonumber(redis.call(get, KEYS[1]) or 0) local bought tonumber(redis.call(get, KEYS[2]) or 0) if stock tonumber(ARGV[1]) then return -1 end if bought tonumber(ARGV[1]) tonumber(ARGV[2]) then return -2 end redis.call(decrby, KEYS[1], tonumber(ARGV[1])) redis.call(incrby, KEYS[2], tonumber(ARGV[1])) redis.call(expire, KEYS[2], 86400) return 1这段脚本里面有两个很关键的细节。第一个是stock ARGV[1]的判断。这里用了而不是是因为如果库存正好等于购买数量这两张票恰好都是最后两张必须放行如果库存小于购买量则直接拒绝。这样就能避免系统还剩 1 张票但用户要买 2 张的情况继续往下走。第二个是用户限购的 key 过期时间设置。我说过限购周期是按场次来的所以 key 有效期设置成 86400 秒一天覆盖整个售票周期避免用户分多次下单绕过限购。不过这里也暴露了一个问题如果限购按场次算一场演出持续好几天售票这个 key 在 24 小时后会过期用户又能继续买。所以场次开票时间超过 1 天的限购 key 的过期时间应该手动设置成场次结束时间或者用场次 ID 加开票日期做 key。SpringBoot 中调用这段 Lua 脚本的方式如下Autowired private StringRedisTemplate redisTemplate; public boolean tryDeductStock(Long sessionId, Long userId, Integer count) { DefaultRedisScriptLong script new DefaultRedisScript(); script.setLocation(new ClassPathResource(deductStock.lua)); script.setResultType(Long.class); ListString keys Arrays.asList( ticket:stock: sessionId, ticket:user: sessionId : userId ); Long result redisTemplate.execute(script, keys, String.valueOf(count), String.valueOf(MAX_TICKET_PER_USER)); return Long.valueOf(1).equals(result); }3.2 防超卖的三道防线即使有了 Redis 预扣减也不能完全保证数据库层面不会超卖。因为座位状态更新和订单生成是异步的如果消息队列消费失败Redis 扣了库存但数据库没扣对账后库存又回补了——逻辑上是对的。但还有一种极端情况同一个座位的两条消费消息在消费者线程里并发执行两条都执行UPDATE seat SET status 1, order_no ? WHERE id ? AND status 0如果数据库隔离级别没控制好就可能出现两个订单改同一个座位。我在系统里加了三条防线第一道是 Redis Lua 脚本原子扣库存这一步挡掉 95% 的超卖第二道是 MySQL 座位表的乐观锁版本号更新座位状态时带上AND version ?更新成功后version 1这样并发更新时只有一个能成功第三道是唯一索引兜底订单明细表对(order_no, seat_id)建了唯一索引如果同一座位被插入两次订单明细数据库直接报错拒绝。三道防线不是虚荣而是每一层都在解决不同粒度的问题。Redis 解决的是库存数字层面的并发扣减乐观锁解决的是同一行数据的并发更新唯一索引解决的是最坏情况下的数据约束兜底。我从做过的几个项目中总结的经验是高并发系统里永远不要相信任何一层逻辑是绝对可靠的多一层兜底上线后就能少接几个投诉电话。3.3 RabbitMQ 异步落库与消息可靠性Redis 扣减成功之后用户的请求其实已经可以返回抢票成功正在确认订单了。我选择通过 RabbitMQ 将创建订单的消息异步发给消费者消费者再执行 MySQL 写入。这样做的好处前面已经说过关键是消息本身的可靠性要怎么保证。// 发送方产生订单消息 rabbitTemplate.convertAndSend( ticket.order.exchange, ticket.order.create, new OrderCreateMessage(sessionId, userId, seatIds, orderNo) );消费者端我做了几件重要的事一是开启手动 ACK处理成功后调用channel.basicAck处理失败且确认是临时性问题时调用basicNack并requeuetrue重新入队处理失败且是业务错误比如座位已经被别人买走时basicNack且requeuefalse转入死信队列二是消费逻辑做了幂等订单表里订单号有唯一索引重复投递消息时不会重复插入订单而是直接返回成功三是消费失败后要主动回补 Redis 库存否则会出现用户最终没买成票但 Redis 库存被扣了不释放的问题。这里有一个我踩过坑的地方手动 ACK 下requeuetrue要慎用。如果消费者因为代码 bug 一直处理失败消息会无限循环消费堆积 CPU 和数据库压力。更合理的做法是设置重试次数上限超过重试次数直接转入死信队列让运营人员查原因后再手动处理。我在项目里配置了spring.rabbitmq.listener.simple.retry.enabledtrue、max-attempts3并设置死信队列专门收集失败消息。3.4 接口限流与降级保护抢票接口必须做限流不然单纯的预扣减也扛不住恶意脚本的疯狂请求。我在网关层基于 Sentinel 做了简单的 QPS 限流每个用户每秒最多 5 次抢票请求超过直接返回操作太频繁。另外针对整场演唱会我用令牌桶算法在 Redis 里做了个全局限流器每秒最多放行 1000 个抢票请求其余请求返回当前抢票人数过多请稍后重试。降级策略也很重要。如果 RabbitMQ 消息积压超过阈值说明消费者处理不过来这时候直接对用户端返回一个系统繁忙订单确认中的提示而不是继续生成阻塞请求。同时在 Redis 中标记一个降级开关字段抢票接口读这个开关一旦打开就进入半关闭状态新请求直接排队不处理避免整个链路雪崩。4. 前后端联调与 SpringBoot 工程实践这个项目的后端基于 SpringBoot 2.7.18JDK 用的是 1.8原因很现实生产服务器的系统镜像里预装的就是 JDK 1.8换新版本还要改运维脚本成本不值当。SpringBoot 版本选 2.7.x 而不是 3.x一方面是为了兼容 JDK 1.8另一方面是公司内部很多基础组件比如自研的监控 SDK还没有适配 SpringBoot 3 的 Jakarta EE 命名空间。如果你的项目完全从零开始且没有历史包袱可以直接上 SpringBoot 3 JDK 17但如果有老系统要兼容老老实实 2.7.x 更稳。4.1 为什么必须用 SpringBoot 自动装配SpringBoot 最核心的机制是自动装配。简单说它通过spring.factories或AutoConfiguration.imports文件在项目启动时自动加载大量默认配置。比如你引入了spring-boot-starter-data-redisSpringBoot 会自动帮你创建RedisTemplate、StringRedisTemplate的 Bean不需要写任何 Bean 方法。这个机制极大地缩短了项目搭建时间。但自动装配也容易踩坑。有时候你引入了一个 starter它自动装配了一个你没想要的 Bean导致启动报错或者 Bean 冲突。排查思路是看启动日志里的Negative matches和Positive matches部分——Positive matches表示哪些自动配置生效了Negative matches表示哪些配置不生效。比如RedisAutoConfiguration在Positive matches里说明 Redis 自动配置生效了。4.2 前后端分离下的 CORS 和鉴权配置项目前端分两个工程用户端跑在8080端口管理端跑在8081端口后端统一跑在8080端口外的9090端口。这就会遇到跨域问题。我用了 SpringBoot 的全局 CORS 配置Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }setAllowCredentials(true)是允许携带 Cookie但要注意allowedOriginPattern(*)和setAllowCredentials(true)在 SpringBoot 2.4 之后才能这样一起用如果用addAllowedOrigin(*)就会报错。这个坑花了我一个下午才查清楚。鉴权用的是 JWT用户登录后签发一个 30 分钟有效的 token前端每次请求放在Authorization头里。后端拦截器解析 token把用户 ID 塞进ThreadLocal或请求上下文。这里要特别注意Swagger 和用户登录接口必须放行否则没法调试。我在拦截器里维护了一个白名单数组包含/api/auth/login、/doc.html、/webjars/**、/v3/api-docs/**等路径剩下的统一拦截校验。4.3 单元测试与事务控制的几个坑项目里我写了一些针对核心链路的单元测试覆盖了 Lua 脚本扣减库存、Redis 扣减后回补、订单超时释放座位等场景。测试单个方法很简单但测试涉及到事务的 Service 方法时要特别小心Transactional与this调用的问题。很多新手会这么写在同一个类里一个业务方法调用另一个带Transactional的方法结果发现事务没有生效。原因在于 Spring 的声明式事务是基于 AOP 代理实现的this调用走的是原始对象而不是代理对象所以事务注解就失效了。解决办法是注入自身代理或者把被调用的方法抽到另一个 Service 类里。我以前遇到过类似问题调试了老半天才发现是内部调用导致的事务失效。这类问题在 SpringBoot 里非常常见建议面试和实际开发都要留意。5. 常见问题与排查技巧实录一次完整交付后我把实际开发中遇到的典型问题和排查过程整理成了一个速查表方便之后团队接手和自己复盘。每个问题都是真实的线上场景花了不少时间定位。5.1 SpringBoot 版本引发的兼容性问题近期网上关于SpringBoot 版本太高的讨论很多我在项目里也真实感受到了版本升级带来的兼容性成本。SpringBoot 3.x 基于 Spring Framework 6底层把javax.*包迁移到了jakarta.*如果你用了老版本的 MyBatis、PageHelper 或者一些自研工具包这些组件很多还停留在javax时代直接升级 SpringBoot 3 会导致编译不过或者运行时报ClassNotFoundException: javax.servlet.*。我的建议是除非你有充分的理由否则新项目先确认一下团队的依赖环境。比如我们项目里用了 MyBatis-Plus 3.5.3它同时兼容 SpringBoot 2 和 3但需要分别引入不同后缀的 starter。还有一次项目里把spring-boot-starter-parent版本从 2.7.18 升到 3.2.0结果springfox-swagger2直接没法用了因为 SpringFox 官方停更不兼容 SpringBoot 3。后来我干脆换成了springdoc-openapi同时把 Swagger 配置从EnableSwagger2改成了OpenAPIDefinition。5.2 从循环依赖到事务失效项目里最开始为了省事把订单服务和座位服务写成了相互调用结果一启动就报错The dependencies of some of the beans in the application context form a cycle这是典型的 Spring 循环依赖问题。SpringBoot 2.6 之后默认禁止循环依赖启动时会直接报错。解决办法不是去设置spring.main.allow-circular-referencestrue而是从设计上解决循环依赖。我把座位服务和订单服务的互相调用改成了通过事件驱动解耦座位锁定成功后发布一个SeatLockedEvent订单服务监听事件创建订单反过来订单取消时发布OrderCancelledEvent座位服务监听后释放座位。这样代码结构更清晰也避免了循环依赖。另一个高频问题是事务失效。我最初写了一个createOrderAndLockSeat()方法内部调用了seatService.lockSeat()和orderService.createOrder()都给这两个方法加了Transactional。测试时发现如果createOrder()失败已经锁定好的座位没有被回滚。排查后发现问题出在事务的传播行为上两个方法分别在两个类里默认的传播行为是REQUIRED理论上应该合并到同一个事务但我在lockSeat()方法里异常时用了try-catch把异常吞掉了导致事务感知不到异常自然不会回滚。这是一个看起来不起眼但非常容易犯的错一定要记得Transactional 只在异常抛出时回滚不能捕获异常内部处理后不重新抛出。5.3 常见的 MyBatis 集成问题处理开发中eclipse 里 SpringBoot 集成 MyBatis 一直报错downloading...是网上很常见的问题。这个场景我遇过很多次大多是 Maven 仓库依赖下载不完整或镜像源太慢导致的。处理方法很简单检查本地仓库的.lastUpdated后缀文件删除后换阿里云镜像重新拉取。另外要确认你引入的 MyBatis starter 正确。mybatis-spring-boot-starter的 groupId 是org.mybatis.spring.boot和 Spring 官方 starter 不是一个组织很多人写错了导致一直下载不了。5.4 抢票接口的压测问题定位上线前压测发现并发 3000 时服务能扛住但订单生成速度一直在下滑RabbitMQ 的队列堆积越来越多。后来查了 RabbitMQ 管理页面发现消费者的prefetch默认是 250也就是说每个消费者一次性拉取 250 条消息到本地内存还没处理完就全部锁定了消息处理的实时性很差。把prefetch改成 10 之后消费速率立刻上来了队列堆积也缓解了。这是一个很多人不会注意但影响很大的参数。还遇到过一个诡异的问题用户抢票成功后支付跳转页面显示订单状态还是待支付但数据库里已经有支付流水了。最后发现是支付回调接口被之前的用户线程抛出的异常影响了回调接口里对重复回调的处理逻辑不严谨没有先更新订单状态再更新支付流水。通过把支付回调逻辑改成先查订单状态、幂等更新、最后记录流水问题解决。6. 个人经验与后续扩展建议整套系统从设计到落地我最强烈的感受是做高并发业务系统先想清楚失败场景再想正常流程。我在写抢票流程时先列了一堆如果这里挂了会怎样的问题比如 Redis 挂了怎么办、MQ 挂了怎么办、数据库连接池爆了怎么办、库存扣了但订单没生成怎么办。把这些失败的路径全部设计好处理方式后代码再往下写就非常顺了。关于扩展我觉得这个系统后续有几个方向可以做得更深。一是引入更细粒度的座位分区和价格策略比如不同区域自动调价、早鸟票、套票等玩法二是加入风控系统识别恶意抢票的马甲号同一设备 ID 频繁切换账号、IP 段集中等三是把订单查询和管理做成独立的读服务用 Canal 监听 MySQL binlog 同步数据到 Elasticsearch提升管理端的搜索和报表性能。这些都属于逐步演进的事情核心的抢票链路和库存一致性方案在三周内已经达到了可上线状态后续的优化是根据业务发展逐步迭代的。最后分享一个小技巧在抢票高峰来临之前提前用脚本把热场次的库存和限购 key 预热到 Redis避免瞬间大量请求同时去数据库加载库存。我在项目里配置了一个PostConstruct的初始化任务项目启动时自动把当天所有在售票场次的库存加载到 Redis实测下来非常管用。如果你的系统要上量这个细节一定要加。
返回列表