Skip to main content

4. Design Flash Sale & Booking System

info

设计秒杀系统 (AKA. Seckill System) 与订票系统

Overview

  • Step1. Scenario 场景
    • 需要设计哪些功能,有哪些场景
    • Ask / Features / QPS / DAU / Interfaces
  • Step2. Service 服务
    • 将大系统拆分为小服务
    • Split / Application / Module
  • Step3. Storage 存储
    • 数据如何存储和访问
    • Schema / Data / SQL / NoSQL / File System
  • Step4. Scale 升级
    • tradeoff 取舍并解决缺陷,处理可能遇到的问题
    • Sharding / Optimize / Specical Case

Step1. Scenario

  • 订票 订酒店
  • 抢购商品手机等

QPS 分析

  • 平日每秒 1000 人访问该页面
  • 秒杀时每秒数 10 万人访问该页面
  • QPS 增加 100 倍以上

流程分析

需要解决的问题

  • Large flow and high concurrency
    • 瞬时大流量高并发
    • 服务器、数据库等能承载的 QPS 有限,如数据库一般是单机 1000 QPS。需要根据业务预估并发量。
  • Over sale
    • 有限库存,不能超卖
    • 秒杀的商品种类是比较少的
    • 库存是有限的,需要精准地保证,就是卖掉了 N 个商品。不能超卖,当然也不能少卖了。
  • Malicious ticket Grab
    • 黄牛恶意请求
    • 使用脚本模拟用户购买,模拟出十几万个请求去抢购。
  • Fixed time
    • 固定时间开启。以服务器时间为准
    • 时间到了才能购买,提前一秒都不可以
  • Purchase limit
    • 严格限购
    • 一个用户,只能购买 1 个或 N 个。

需求拆解

  • 商家侧
    • 新建秒杀活动
    • 配置秒杀活动
  • User
    • App page (Web / Mobile)
    • Order
    • Pay

Step2. Service

单体架构

微服务架构

Step3. Storage

SQL 表设计

Data flow

  • User
    • select: 查询 seckill_info, commodity_info, stock_info表,将结果渲染到 app
    • insert: 插入数据到 order_info
    • update: 购买成功后,更新 stock_info

数据库场景

Order request

-- 1. 事务开始
START TRANSACTION;

-- 2. 查询库存余量,并锁住数据
SELECT stock FROM `stock_info`
WHERE commodity_id = 189 AND seckill_id = 28 FOR UPDATE;

-- 3. 扣减库存
UPDATE `stock_info` SET stock = stock - 1
WHERE commodity_id = 189 AND seckill_id = 28;

-- end transaction
  • For update是行锁,其他人不能再修改此条数据
  • 保证执行的正确性
  • 事务的执行比较耗时,一般不用此方案
-- select
SELECT stock FROM `stock_info`
WHERE commodity_id = 189 AND seckill_id = 28;

-- update
UPDATE `stock_info` SET stock = stock - 1
WHERE commodity_id = 189 AND seckill_id = 28 AND stock > 0;
  • update 语句自带了行锁,即判断 stock > 0
  • 如果大量的 request 访问 MySQL,导致 MySQL overload
  • 实际工程中,绝大部分的 request 都达到不了 MySQL

Cache - Redis

Warm-up 预热

  • 在活动开始前,从 DB读取秒杀活动和商品信息,将商品库存信息存入 Cache
  • E.g., SET seckill:28:commodity:189:stock 100

Order request

  • 大部分请求都被 Redis 挡住了,实际下沉到 MySQL 的理论上应该就是能创建的订单了。比如只有 100 台 iPhone,那么到 MySQL 的请求量理论上是 100。
  • 获取 key 存储的值 GET seckill:28:commodity:189:stock
  • 将 key 中存储的值 -= 1 DECR seckill:28:commodity:189:stock#
  • Problem
    • 检查 Redis 库存和扣减 Redis 库存是两步操作。
    • 有并发问题仍然会导致超卖
  • Solution
    • 如果 Redis 侧放行,可以创建订单了,到 MySQL 的时候也需要再检查一次。
    • 如果并发量超高,Redis 侧实际超卖的量过大,如 100 万个请求同时到达,Redis 全部放行。再到 MySQL 去检测,那 Redis 作用等于没有。

Set Lua script

Peak Clipping 削峰

  • 使用 Message Queue 进行 Async task 异步任务
  • 实现两个不同系统之间的解耦 Decoupling
  • 每个 message 加 sequence number 来防止 duplictes

库存扣减时机

  • 下单时立即减库存
    • 用户体验最好,控制最精准,只要下单成功,利用数据库锁机制,用户一定能成功付款。
    • 可能被恶意下单。下单后不付款,别人也买不了了。
  • 先下单,不减库存。实际支付成功后减库存
    • 可以有效避免恶意下单。
    • 对用户体验极差,因为下单时没有减库存,可能造成用户下单成功但无法付款。
  • 下单后锁定库存,支付成功后,减库存

Data validation 数据校验

Idempotency 幂等性

接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子。如果用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条.这就没有保证接口的幂等性

Step4. Scale

防止前端页面崩溃

  • 前端限流
    • 点击一次后,按钮短时间内置灰
  • 部分的 request 直接跳转到繁忙页
  • 未开始抢购时,禁用抢购按钮
    • 前端轮询 Poll (循环地去获取并校准时间) 服务器的时间,并获取距离活动开始的时间差

服务雪崩 Avalanche

服务熔断 Fuse or Circuit-breaker

熔断机制是应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务不可用或者响应时 间太长时,熔断该节点微服务的调用,快速返回”错误”的响应信息。当检测到该节点微服务响应 正常后恢复调用链路。

  • Netflix Hystrix
  • Alibaba Sentinel

防止恶意请求

  • 验证码机制 Verification Code Mechanism
  • 限流机制 Ratelimit Mechanism
    • 来自同一个 IP address 或者同一个 user id,禁止抢购
  • 黑名单机制 Blacklist Mechanism
    • 黑名单 IP address
    • 黑名单 user id