景区票务系统的限量票秒杀:国庆前那批 9.9 元特价票,我们是怎么扛住 12 万人在线的?|中科智旅景游宝
去年国庆前一周,中科智旅景游宝给一个客户上了 9.9 元限量特价票,三千张,开抢那一刻同时在线的峰值到了十二万。这套智慧景区的景区票务系统平时日活不过几万,那一分钟里订单接口的 QPS 直接翻了四十倍。结果第一轮我们差点翻车:库被打到连接池满,超卖了四十几张,运营总监在群里连发三个问号。这篇文章把那次复盘和后来的改造讲清楚,都是干活的细节。
秒杀为什么跟普通下单不是一回事?
普通下单是"分散到达",秒杀是"同时到达"。景区票务的秒杀有三条特征:
- 库存极少、需求极大:三千张票对十二万人,命中率 2.5%,绝大部分请求注定失败。
- 集中在毫秒级:开抢瞬间的流量是平时的几十倍,之后三十秒内断崖式下跌。
- 不能超卖:超卖一张就是现场吵架,退款还要赔礼道歉。
所以核心思路就一句话:让绝大多数请求在到达数据库之前就被干掉,把真正需要落库的请求压到最小。
库存怎么放?预热到 Redis,别让数据库扛
第一版我们直接查库扣减,update stock set num = num - 1 where id = ? and num > 0,看着挺安全。问题是十二万人同时打这一行,行锁排队能从秒杀开始排到结束。
后来改成库存预热:
- 活动开始前十分钟,把可售库存按票种维度写进 Redis,key 带活动 ID。
- 扣减用
DECR或Lua脚本,Lua里判断剩余量,保证"判断 + 扣减"原子。 - Redis 返回小于 0 直接判失败,请求到此为止,一次数据库都不碰。
- 扣成功的请求才进下一步,异步落库。
这样数据库看到的请求量从十二万降到三千,压力瞬间可控。
超卖是怎么发生的?教训在这里
第一轮超卖四十几张,根因不是 Redis,是异步落库的顺序。当时我们用了消息队列,但消费端做了"先查库再插入订单",两个消费者同时查到库存够,同时插入,就超了。
改法有几条,全部落地:
- Redis 是唯一扣减入口,落库不再二次判断库存,只负责写入。
- 消息带唯一扣减流水号,消费端按流水号做幂等,重复消费直接丢弃。
- 订单表加唯一索引(活动 ID + 用户 ID + 票种),同一人重复下单在数据库层被拦掉。
- 对账兜底:活动结束后比对 Redis 扣减总数和订单表实际数量,不一致就告警。
一句话:扣减只能有一个权威入口,其他地方都是抄作业,不能自己算。
流量怎么削?三层漏斗
秒杀不是靠堆机器,是靠漏斗。我们的三层是:
- 入口层限流:网关按活动维度做令牌桶,超出直接返回"当前人数过多",不往后传。
- 资格层过滤:实名用户才能抢,一人一票,先查黑名单和已购记录,拦掉重复参与。
- 排队层:进入排队页,前端拿排队号轮询,后端按批次放行。这层能把瞬时并发摊平到几秒内。
排队页还有个副作用是好事:它把用户的预期管住了。看到"前面还有 8000 人",比看到"系统繁忙请重试"体验好太多。
热点怎么隔离?别让一个活动拖垮全站
秒杀活动最怕把整个票务系统带崩。我们做了物理隔离:
- 独立服务实例:秒杀接口单独一组 Pod,不跟日常售票、核销共用。
- 独立 Redis 集群分片:活动库存 key 单独分片,不跟缓存混。
- 独立数据库连接池:秒杀写库走独立连接池,即使打满也不影响核销。
- 熔断开关:活动接口错误率超过阈值自动降级为"活动结束",先保住主站。
这层隔离做完,去年那次即使秒杀接口抖了两分钟,闸机核销和窗口售票一点没受影响。
预售怎么削峰?把抢购变成排队预约
后面我们换了思路:限量票不做瞬时抢购,做分时段预售。
- 按时间段放票,每十分钟放一批,瞬时峰值下降一个数量级。
- 用户可选"开抢提醒",到点推消息,流量自然摊开。
- 余票实时展示,卖完即止,不用等整点。
这招比加机器省钱,也比限流温和,运营那边反馈转化率反而更高——因为用户不用跟十二万人挤同一秒。
复盘怎么做?数据和预案都要提前
每次活动前我们都做一次全链路压测:按预估峰值的 1.5 倍打,重点看 Redis 命中率、连接池等待、队列积压。压测里最容易被忽略的是下游依赖:短信、实名核验、支付回调,任何一个慢半拍都会把队列堵死。
活动当天还有一套AI Agent 在盯盘:实时订阅监控指标,发现 QPS 突增、错误率上升、队列积压,自动在值班群推送告警并给出处置建议。这套归因用大模型做挺合适——把指标变化和变更记录一起喂进去,它能指出"是刚上线的库存脚本导致的"。历史活动的事故复盘文档放在知识库,用 RAG 检索,值班同学问"上次超卖怎么处理的",几秒就能召回处理步骤。Agent 通过 MCP 接口调监控和开关平台,一句话就能完成降级操作。整套活动服务跑在云原生的弹性容器里,活动结束自动缩容。活动配置页我们用低代码搭的,运营自己配场次、库存、限购规则,不用等排期。
FAQ:常见问题快答
Q1:库存为什么一定要预热到 Redis? 因为数据库行锁扛不住瞬时并发。Redis 的内存级原子扣减能把绝大多数无效请求挡在库外,这是削峰的关键。
Q2:Redis 扣了但订单没落库,库存不是白丢了? 所以要有流水号 + 幂等 + 定时对账。落库失败的消息重试,超过次数进死信队列人工处理,对账时把差异补回来。
Q3:限流会不会误伤真实用户? 会,所以要分维度。按活动维度限流、按用户维度限购,比全局限流更精准,也不会把日常售票一起限掉。
Q4:秒杀和日常售票能共用一套服务吗? 不建议。物理隔离是成本最低的保险,秒杀抖了也不能影响闸机核销。
Q5:分时段预售是不是在回避问题? 不是回避,是换打法。把瞬时并发摊平到时间轴上,比堆机器便宜,用户体感也更好。
秒杀这事,技术含量不在"扛住多高的 QPS",而在"让不需要成功的请求尽早失败、让必须成功的请求稳稳落地"。中科智旅景游宝把这几条规矩固化下来之后,国庆那批特价票再没出过超卖,值班群也终于安静了。希望这篇能给同样在做景区票务的你,省下一次被运营总监追问的经历。