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
表
- select: 查询
数据库场景
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