|
12306 的打造地难点从来不是“高并发”三个字,而是高并“余票扣减 + 订单创建 + 支付回调”这条链路在几十万人同时点“提交订单”时还能保证不超卖。我见过一个真实案例:某小团队照搬电商秒杀方案,发仿把库存扣减放 Redis,售票结果春运压测时发现同一张票被卖了三次——原因是打造地 Redis 扣减成功但 MySQL 落库失败,没有对账补偿。高并 所以仿 12306 的发仿第一步,是售票把“票”当成有向区间的库存,而不是打造地普通商品。G1234 次列车,高并北京南到上海虹桥,发仿中间停 5 站。售票一张票的打造地本质是“某段区间的占用”。你买北京南到济南西,高并他买济南西到上海虹桥,发仿这两张票可以同时卖,因为区间不重叠。但如果你买北京南到上海虹桥,整条线路的库存都得扣。 我自己的做法是:用“位图 + 原子操作”来管理区间库存。每趟车每个座位类型维护一个 bitset,长度等于区间段数。比如 6 个站就有 5 段,bitset 为 11111 表示全空。买北京南到济南西,就是把第 0 段置 0。用 Redis 的 但 Redis 只做“预扣”。真正落库前,还得靠数据库唯一索引兜底。订单表里加一个 订单创建环节,我坚持“先落库再返回”。很多人为了压测数据好看,把订单写进 Redis 就返回成功,结果用户支付完发现订单不存在。12306 这种场景,用户容忍度极低。我的做法是:MySQL 分库分表,按车次日期哈希,单表控制在 2000 万行以内。插入订单用 支付回调是另一个坑。微信/支付宝的回调可能重复,也可能乱序。我的处理是:回调表加 压测时别只看 QPS。我关注三个指标:Redis 位图冲突率、MySQL 唯一索引冲突率、订单状态机回滚率。冲突率超过 5% 说明预扣逻辑有问题,回滚率超过 1% 说明支付回调有重复。有一次我把 Redis 位图 key 的过期时间设成 10 分钟,结果用户选座后犹豫了 12 分钟才提交,位图已过期,库存被重复扣减。后来改成“位图不过期,靠延迟消息回补”,问题消失。 最后说个反直觉的点:别用分布式锁。我试过用 Redisson 锁座位,QPS 直接掉到 800。后来改成“位图原子操作 + 唯一索引兜底”,单节点 Redis 扛住 3 万 QPS 预扣,MySQL 分 16 库后写入也没压力。仿 12306 的核心不是堆中间件,是把“区间库存”这个模型吃透,然后用最笨的数据库约束兜底。 |
