本文目录导读:

- 项目背景与业务痛点
- 系统总体架构设计
- 高并发秒杀核心设计(关键步骤)
- 核心数据库表设计(精简版)
- 防黄牛与风控体系
- 异常场景处理与容灾
- 性能指标测试参考
- 技术栈汇总
- 总结:该案例的亮点(面试展示重点)
这是一个经典的票务系统案例设计文档,我会以一个大型演唱会/体育赛事的票务系统为背景,从业务痛点、系统架构、核心流程、数据库设计、高并发解决方案以及防黄牛/防刷等维度进行拆解。
你可以直接参考这个案例来进行系统设计面试或作为项目原型。
项目背景与业务痛点
背景: 某头部主办方(如“周杰伦演唱会”或“NBA中国赛”)开票,瞬间涌入百万级用户抢购十万张门票。
核心痛点:
- 超卖: 数据库库存扣减错误,导致多卖票(无法交付)。
- 雪崩: 瞬间高并发打垮订单服务,导致系统宕机(一票难求)。
- 黄牛: 机器脚本刷票,导致真实用户抢不到票。
- 抢座冲突: 高并发下,同一个人座位被多人同时锁定。
系统总体架构设计
为了保证高可用、高并发,采用微服务 + 分布式缓存 + 异步削峰的架构。
[客户端]
(APP/H5/小程序)
|
| HTTPS + CDN
v
[负载均衡层] Nginx / 阿里云SLB
|
v
[接入网关层] Spring Cloud Gateway (限流/路由/风控)
|
|------------------------------|
| |
v v
[用户服务] [票务核心服务] ---- [风控服务/黑名单]
|
| (秒杀专用)
v
[Redis 集群]
(库存预热 + 分布式锁)
|
| 异步削峰 (MQ)
v
[RabbitMQ / RocketMQ]
|
[订单消费服务] (串行化落库)
|
v
[MySQL 主库]
(订单表 + 库存表 - 乐观锁)
高并发秒杀核心设计(关键步骤)
秒杀的难点在于“读多写少”,前端读(查询库存/座位)可以走CDN缓存,关键是扣减库存。
库存预热(提前放入Redis)
- 在开票前1小时,将门票的
stock字段载入Redis。 - Redis Key 设计:
ticket:stock:{sessionId}->100000(整数)。
原子扣减库存(Lua脚本防并发)
- 使用Redis的Lua脚本来保证“检查库存”和“扣减库存”的原子性,防止超卖。
- 核心逻辑(伪代码):
-- 若库存大于0,则减1 local stock = redis.call('GET', KEYS[1]) if tonumber(stock) <= 0 then return -1 -- 卖完了 end redis.call('DECR', KEYS[1]) return 1 -- 扣减成功
限流与防刷(网关层)
- 单用户限流: 同一个UID,1秒内仅允许1次抢购请求进入后端。
- 设备指纹: 检测异常高频请求的IP或设备ID,直接返回“请求过于频繁”。
异步下单(削峰填谷)
- 扣减Redis库存成功后,不直接写MySQL(不然数据库瞬间压力爆炸),而是发送一条
MQ消息。 - 后端订单消费者(消费速度恒定)从MQ拉取消息,进行MySQL的最终扣减与订单生成(状态:待支付)。
- 注: 这里的MySQL扣减也必须使用乐观锁
UPDATE stock SET version = version + 1 WHERE id = ? AND version = ?作为最终兜底。
用户态轮询
- 抢购成功后,前端不刷新页面等待,而是轮询订单服务接口,查询订单是否已生成,若生成跳转支付页。
核心数据库表设计(精简版)
票档表(ticket_sku)
| 字段名 | 类型 | 说明 |
|---|---|---|
id |
bigint | 主键(票档ID,如:内场A区) |
session_id |
bigint | 场次ID |
name |
varchar | 票档名称(看台/内场) |
price |
decimal | 售价 |
total_stock |
int | 总库存 |
version |
int | 乐观锁版本号(防超卖兜底) |
订单表(orders)
| 字段名 | 类型 | 说明 |
|---|---|---|
id |
bigint | 主键 |
order_no |
varchar | 全局唯一订单号(雪花算法) |
user_id |
bigint | 用户ID |
sku_id |
bigint | 票档ID |
status |
tinyint | 0待支付 / 1已支付 / 2已取消 |
create_time |
datetime | 下单时间 |
座位锁定表(seat_lock)(如为选座系统)
| 字段名 | 类型 | 说明 |
|---|---|---|
id |
bigint | 主键 |
session_id |
bigint | 场次ID |
row_no |
int | 排号 |
seat_no |
int | 座号 |
user_id |
bigint | 锁定用户 |
status |
tinyint | 0空闲 / 1锁定 / 2已售 |
lock_time |
datetime | 锁定时间(超过10分钟未支付自动释放) |
防黄牛与风控体系
- 强实名制: 下单时绑定身份证,入场时人脸识别比对。
- 风控规则引擎:
- 同一收货地址下单超过N次 -> 触发拦截。
- 同一IP段/设备ID高频请求 -> 加入黑名单返回错误码。
- 支付账号与实名信息不一致 -> 拦截。
- 行为验证: 在提交订单前加入滑块验证(如极验)或点选验证,增加机器脚本的破解成本。
- 异步检测: 对于“秒杀成功”的账号,后台异步分析其历史行为(如:是否刚注册、是否有退票记录),若判定为异常,则强制取消订单(熔断机制)。
异常场景处理与容灾
-
用户重复点击(幂等性):
- 前端: 点击抢票后按钮置灰。
- 后端: 使用
userId + skuId生成幂等Key,存入Redis(SETNX),若已存在则不重复生成订单。
-
库存超卖兜底:
- 第一层: Redis Lua原子减。
- 第二层: MySQL
UPDATE ... WHERE total_stock > 0(行锁),即便Redis挂掉,数据库也会挡住超卖。
-
支付超时:
- 使用延迟队列(RocketMQ延迟消息)或定时任务(xxl-job)扫描“待支付”订单。
- 若超时15分钟未支付,则将Redis中的库存回滚+1,并释放座位锁定,供其他用户抢购。
性能指标测试参考
- QPS 压测目标: 核心抢票接口支撑 10万+ QPS(通过Redis支撑)。
- 下单延迟: 30秒内完成订单异步落库(保证不阻塞抢购主流程)。
- 成功率: 系统可用性达到 99%(避免因流量洪峰导致雪崩)。
- 端到端延迟: 用户点击抢票,在 2 - 3 秒内响应“抢票成功/失败”提示。
技术栈汇总
| 组件 | 技术选型 | 用途 |
|---|---|---|
| 负载均衡 | Nginx | 接入层入口,动静分离 |
| 微服务框架 | Spring Boot + Spring Cloud Alibaba | 业务逻辑实现 |
| 缓存中间件 | Redis (Lua脚本) | 库存扣减,分布式锁 |
| 消息队列 | RocketMQ / RabbitMQ | 异步下单、流量削峰 |
| 数据库 | MySQL (InnoDB) + MyBatis-Plus | 最终库存和订单持久化 |
| 分布式追踪 | Zipkin / SkyWalking | 排查高并发下的全链路瓶颈 |
| 监控告警 | Prometheus + Grafana | 监控业务QPS、服务资源、Redis内存 |
该案例的亮点(面试展示重点)
回答思路: “在构建该系统时,我主要关注了数据一致性和高可用两点,针对高并发秒杀场景,我没有采用传统的数据库行级锁(会导致阻塞耗时),而是采用了 ‘Redis预扣减 + MQ异步落库 + 数据库乐观锁兜底’ 的三级方案,通过Lua脚本保证原子性,成功将数据库的写压力从百万级瞬间并发,降级为平稳的异步消费,在业务上利用IP限流和幂等性设计,有效解决了超卖和黄牛刷单的痛点。”
如果你需要,我可以进一步为你细化某一个模块的设计,
- 如果是“高铁/电影”这种“锁座选座”的场景,怎么处理锁冲突和死锁?
- 如何设计一个通用的“延迟消息队列”来处理未支付订单的回滚?
告诉我你的侧重点,我可以继续展开。