票务系统案例

wen java案例 2

本文目录导读:

票务系统案例

  1. 项目背景与业务痛点
  2. 系统总体架构设计
  3. 高并发秒杀核心设计(关键步骤)
  4. 核心数据库表设计(精简版)
  5. 防黄牛与风控体系
  6. 异常场景处理与容灾
  7. 性能指标测试参考
  8. 技术栈汇总
  9. 总结:该案例的亮点(面试展示重点)

这是一个经典的票务系统案例设计文档,我会以一个大型演唱会/体育赛事的票务系统为背景,从业务痛点、系统架构、核心流程、数据库设计、高并发解决方案以及防黄牛/防刷等维度进行拆解。

你可以直接参考这个案例来进行系统设计面试或作为项目原型。


项目背景与业务痛点

背景: 某头部主办方(如“周杰伦演唱会”或“NBA中国赛”)开票,瞬间涌入百万级用户抢购十万张门票。

核心痛点:

  1. 超卖: 数据库库存扣减错误,导致多卖票(无法交付)。
  2. 雪崩: 瞬间高并发打垮订单服务,导致系统宕机(一票难求)。
  3. 黄牛: 机器脚本刷票,导致真实用户抢不到票。
  4. 抢座冲突: 高并发下,同一个人座位被多人同时锁定。

系统总体架构设计

为了保证高可用、高并发,采用微服务 + 分布式缓存 + 异步削峰的架构。

[客户端]  
  (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分钟未支付自动释放)

防黄牛与风控体系

  1. 强实名制: 下单时绑定身份证,入场时人脸识别比对。
  2. 风控规则引擎:
    • 同一收货地址下单超过N次 -> 触发拦截。
    • 同一IP段/设备ID高频请求 -> 加入黑名单返回错误码。
    • 支付账号与实名信息不一致 -> 拦截。
  3. 行为验证: 在提交订单前加入滑块验证(如极验)或点选验证,增加机器脚本的破解成本。
  4. 异步检测: 对于“秒杀成功”的账号,后台异步分析其历史行为(如:是否刚注册、是否有退票记录),若判定为异常,则强制取消订单(熔断机制)。

异常场景处理与容灾

  1. 用户重复点击(幂等性):

    • 前端: 点击抢票后按钮置灰。
    • 后端: 使用 userId + skuId 生成幂等Key,存入Redis(SETNX),若已存在则不重复生成订单。
  2. 库存超卖兜底:

    • 第一层: Redis Lua原子减。
    • 第二层: MySQL UPDATE ... WHERE total_stock > 0(行锁),即便Redis挂掉,数据库也会挡住超卖。
  3. 支付超时:

    • 使用延迟队列(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限流和幂等性设计,有效解决了超卖和黄牛刷单的痛点。”


如果你需要,我可以进一步为你细化某一个模块的设计,

  • 如果是“高铁/电影”这种“锁座选座”的场景,怎么处理锁冲突和死锁?
  • 如何设计一个通用的“延迟消息队列”来处理未支付订单的回滚?

告诉我你的侧重点,我可以继续展开。

抱歉,评论功能暂时关闭!