秒杀系统场景

2020 年6 月18 日 0 点开始,京东自营限量 100
台,以 4000 元的价格,抢购 iPhone 11 64G 版
本,先到先得,一人限购一台,售完即止。
微信抢红包
抢春运火车票
抢购小米手机
关键词:0 点开始、限量 100 台、一人限购一台

QPS分析

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

商品购买和下单流程

秒杀系统设计 - 图1

需要解决的问题

瞬时大流量高并发

服务器、数据库等能承载的 QPS 有限,如数据库一般是单机 1000 QPS。需要根据业务预估并发量。

有限库存,不能超卖

库存是有限的,需要精准地保证,就是卖掉了 N 个商品。不能超卖,当然也不能少卖了。

黄牛恶意请求

使用脚本模拟用户购买,模拟出十几万个请求去抢购。

固定时间开启

时间到了才能购买,提前一秒都不可以(以商家「京东」「淘宝」的时间为准。

严格限购

一个用户,只能购买 1 个或 N 个。

需求拆解

秒杀系统设计 - 图2

架构设计

单体架构

秒杀系统设计 - 图3
优点:

  1. 部署简单 :由于是完整的结构体,可以直接部署在一份服务器上即可
  2. 技术单一 :项目不需要复杂的技术栈,往往一套熟悉的技术栈就可以完成开发
  3. 用人成本低 :单个程序员可以完成业务接口道数据库的整个流程

缺点:

  1. 前后端耦合,服务压力较大。
  2. 各功能模块耦合严重
  3. 系统复杂,一个模块的升级需要导致整个服务都升级。
  4. 扩展性差,难以针对某个模块单独扩展。
  5. 开发协作困难,各个部门的人都在开发同一个代码仓库。
  6. 级联故障,一个模块的故障导致整个服务不可用。
  7. 陷入某种单一技术和语言中。
  8. 数据库崩溃导致整个服务崩溃。

    微服务架构

    秒杀系统设计 - 图4
    优点:

  9. 各功能模块解耦,保证单一职责。

  10. 系统简单,升级某个服务不影响其他服务。
  11. 扩展性强。可对某个服务进行单独扩容或缩容。
  12. 各个部门协作明晰。
  13. 故障隔离。某个服务出现故障不完全影响其他服务。
  14. 可对不同的服务选用更合适的技术架构或语言。
  15. 数据库独立,互不干扰。

缺点:

  1. 分布式部署,调用的复杂性高
  2. 独立的数据库,分布式事务的挑战
  3. 测试的难度提升
  4. 运维难度的提升

    数据库表设计

    商品信息表
    commodity_info
商品id
id
商品名称
name
商品描述
desc
价格
price
189 iPhone 11 64G xxxxxxxx 5999

库存信心表
stock_info

库存id
id
商品id
commodity_id
活动id
seckill_id
库存
stock
锁定
lock
1 189 0 1000000 0
2 189 28 100 5

秒杀活动表
seckill_info

秒杀id
id
秒杀名称
name
商品id
commodity_id
价格
price
数量
number
28 618 iPhone 11 64G秒杀 189 4000 100

订单信息表
order_info

订单id
id
商品id
commodity_id
活动id
seckill_id
用户id
user_id
是否付款
paid
1 189 28 Jack 1

数据流 - 活动图

秒杀系统设计 - 图5

秒杀操作 - 扣减库存

读取判断库存,然后扣减库存

  1. 查询库存余量

    1. select stock from stock_info where commodity_id = 189 and seckill_id = 28;
  2. 扣减库存

    1. update stock_info set stock = stock - 1 where commodity_id = 189 and seckill_id = 28;

    出现并发超卖问题,在数据库方面的解决方案:

    读取和判断过程加上事务

  3. 事务开始

    1. begin transaction;
  4. 查询库存余量,并锁住数据

    1. select stock from stock_info where commodity_id = 189 and seckill_id = 28 for update;
  5. 扣减库存

    1. update stock_info set stock = stock - 1 where commodity_id = 189 and seckill_id = 28;
  6. 事务提交

    1. commit;

    使用update语句自带的行锁

  7. 查询库存余量

    1. select stock from stock_info where commodity_id = 189 and seckill_id = 28;
  8. 扣减库存

    1. update stock_info set stock = stock - 1
    2. where commodity_id = 189 and seckill_id = 28 and stock > 0;

    秒杀操作 - 库存预热

    概念

    秒杀的本质,就是对库存的抢夺。
    每个秒杀的用户来都去数据库查询库存校验库存,然后扣减库存,导致数据库崩溃。
    MySQL 数据库单点能支撑 1000 QPS,但是 Redis 单点能支撑 10万 QPS,可以考
    虑将库存信息加载到 Redis 中。
    直接通过 Redis 来判断并扣减库存。

    什么时候进行预热

    秒杀系统设计 - 图6
    语法: SET KEY VALUE
    作用:设置指定key的值
    SET seckill:28:commodity:189:stock 100

    通过Redis扣减库存

    秒杀系统设计 - 图7
    语法: GET KEY
    作用: 获取key存储的值
    语法: DECR KEY
    作用:将key中储存的数字值减一
    GET seckill:28:commodity:189:stock
    DECR seckill:28:commodity:189:stock
    问题:
    大部分请求都被 Redis 挡住了,实际下沉到 MySQL 的理论上应该就是能创建的订单了。比如只有 100 台 iPhone,那么 到MySQL 的请求量理论上是 100。
    但是,在检查Redis库存和扣减Redis库存是两步操作。一旦出现并发问题,仍然会出现超卖问题。
    解决方案:
    哪怕Redis侧放行,可以创建订单了,到MySQL的时候也需要再检查一次。
    新的问题:
    如果并发量超高,Redis侧实际超卖的量过大,如100万个请求同时到达,Redis全部放行。再到MySQL检查,那么Redis作用等于没有。
    解决方案:
    通过Lua脚本执行原子操作

    Lua 脚本功能是 Reids 在 2.6 版本中 推出, 通过内嵌对 Lua 环境的支持, Redis 解决了长久以来不能高效地处 理 CAS (check-and-set)命令的缺 点, 并且可以通过组合使用多个命 令, 轻松实现以前很难实现或者不能 高效实现的模式。 Lua 脚本是类似 Redis 事务,有一定 的原子性,不会被其他命令插队,可 以完成一些 Redis 事务性的操作。

  1. if(redis.call('exists', KEYS[1]==1)) then
  2. local stock = tonumber(redis.call('get', KEYS[1]));
  3. if (stock <= 0) then
  4. return -1
  5. end;
  6. redis.call('decr', KEYS[1]);
  7. return stock - 1;
  8. end;
  9. return -1;

秒杀系统设计 - 图8
问题:
如果秒杀数量是1万台,或者10万台呢?
因为 Redis 和 MySQL 处理能力的巨大差异。实际下沉到 MySQL 的量还是巨大,MySQL 无法承受。
解决思路 :
可不可以在通过 Redis 扣库存后,到 MySQL 的请求慢一 点?
通过消息队列(Message Queue,MQ)进行削峰(Peak Clipping)操作。

秒杀操作 - 通过消息队列异步创建订单

秒杀系统设计 - 图9
问题:
如果消息队列出现部分投递失败?
Redis中的库存量,可以比实际的库存量多一点,比如1.5倍或者2倍。

库存扣减时机

下单时立即减库存。

优点:用户体验最好,控制最精准,只有下单成功,利用数据库锁机制,用户一定能成功付款。
缺点:可能被恶意下单。下单后不付款,别人也买不了。

先下单,不减库存。实际支付成功后减库存。

优点:可以有效避免恶意下单。
缺点:对用户体验极差,因为下单时没有减库存,可能造成用户下单成功但无法付款。就是商品库存没有了,在支付阶段,用户就无法付款了。

下单后锁定库存,支付成功后,减库存。(主流)

如何限购

MySQL数据校验

秒杀系统设计 - 图10

Redis数据校验

秒杀系统设计 - 图11
语法:SADD KEY VALUE1…..VALUEN
作用:将一个或多个成员元素加入到集合中,已经存在于集合的成员元素将被忽略。
SADD seckill:28:commodity:189:user uid1
语法:SISMEMBER KEY VALUE
作用:判断成员元素是否是集合的成员。
SISMEMBER seckill:28:commodity:189:user uid1

付款和减库存的数据一致性

秒杀系统设计 - 图12
保证多个存在于不同的数据操作,要么同时成功,要么同时失败。(分布式锁)

防止刷爆商品页面

前端资源静态化

秒杀系统设计 - 图13

前端限流

点击一次后,按钮短时间内置灰
image.png
部分请求直接跳转到【繁忙页】

未开始抢购时,禁用抢购按钮

如何计算倒计时

  1. 打开页面获取活动开始时间,然后前端页面开始倒计时
  2. 打开页面获取距离活动开始的时间差,然后前端页面开始倒计时
  3. 前端轮询 (Poll) 服务器的时间,并获取距离活动开始的时间差

image.png

秒杀服务器挂掉,怎么办

尽量不要影响其他服务,尤其是非秒杀商品的正常购买。
服务雪崩 (Avalanche)
多个微服务之间调用的时候,假设 微服务A 调用 微服务B 和 微服务C,微服务B 和微服务C 又
调用其他的微服务,这就是所谓的”扇出 (Fan-out)”,如扇出的链路上某个微服务的调用响应
式过长或者不可用,对 微服务A 的调用就会占用越来越多的系统资源,进而引起系统雪崩,所
谓的”雪崩效应”。
服务雪崩效应是一种因“服务提供者”的不可用导致“服务消费着”的不可用并将这种不可用
逐渐放大的过程。
image.png
服务熔断 (Fuse or Circuit-breaker)
熔断机制是应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务不可用或者响应时
间太长时,熔断该节点微服务的调用,快速返回”错误”的响应信息。当检测到该节点微服务响应
正常后恢复调用链路。
Netflix Hystrix
Alibaba Sentinel

防止恶意刷请求或者爬虫请求

验证码机制

秒杀系统设计 - 图17

限流机制

秒杀系统设计 - 图18

黑名单机制

  1. 黑名单 IP地址
  2. 黑名单用户ID