UP | HOME

秒杀系统设计

Table of Contents

1 基本参数

  1. 需求设计一个可以承载 100W QPS 的秒杀系统
  2. 考虑到 MySQL 的最大 QPS = 1000
  3. 考虑到 Redis 的最大 QPS = 10W

2 架构设计

在描述活动中涉及到多个微服务的

  1. ActivityService 活动微服务
  2. ProductService 产品微服务
  3. OrderService 订单微服务
  4. PaymentService 支付微服务
[App] ----> ActivityService  --> DB1
       |
       +--> ProductService   --> DB2
       |
       +--> OrderService     --> DB3
       |
       +--> PaymentService   --> DB4

3 数据库设计

3.1 秒杀活动表 activity

create table activity (
  id bigint primary key not null auto_increment,
  activity_code varchar(64) unique not null,
  activity_name varchar(64) not null,
  activity_type varchar(8) not null,
  product_code varchar(64) not null,
  activity_price int not null,
  activity_count int not null
);
活动编码 活动名称 活动类型 商品编码 活动价格 活动数量
H10001 iPhone 2021 秒杀 秒杀 T0001 4499 10
H10002 iPhone 2021 下单增 1000 元优惠卷 买赠 T0001 4999 10

3.2 库存表 stock

create table stock (
  id bigint primary key not null auto_increment,
  product_code varchar(64) unique not null,
  product_name varchar(64) not null,
  activity_code varchar(64) not null,
  stock_count int not null,
  occupy_count int default 0 not null,
  version int default 0
);
商品编码 商品名称 活动编码 总库存数 预占库存数
T0001 iPhone 12 H10001 10 2

3.3 商品表 product

create table product (
  id bigint primary key not null auto_increment,
  product_code varchar(64) unique not null,
  product_name varchar(64) not null,
  product_title varchar(128) not null,
  product_desc text,
  product_price int not null
);
商品编码 商品名称 商品标题 商品原价 商品描述
T0001 iPhone 12 Apple iPhone 12 128GB 白色 移动联通电信 4G 手机 5599 xxxx

4 MySQL 实现扣减库存的途径

4.1 悲观锁 Pessimistic Lock

在 MySQL 的实现方法, 利用 for update 的行锁机制来并发竞争库存

-- 第一步: 查询并锁表
select *
  from stock
 where product_code = ??? for update;

-- 第二步: 扣库存
update stock
   set stock_count = stock_count - 1
 where product_code = ??? and stock_count > 0;

4.2 乐观锁 Optimistic Lock

在 MySQL 的实现方法是引入一个额外的字段,记住更新的版本号

-- 每次查询时添加版本号字段 version, 这种做法不会产生行锁
update stock
   set stock_count = stock_count - 1 and version = version + 1
 where product_code = ??? and stock_count > 0 and version = ???;

-- 如果更新失败, 需要重试一下
select *
  from stock
 where product_code == ???;

update stock
   set stock_count = stock_count - 1 and version = version + 1
 where product_code = ??? and stock_count > 0 and version = ???;

注意:

  1. 在秒杀的情景下乐观锁并不能解决问题
  2. 高并发情景下可能会把 MySQL 数据库整崩溃

5 Redis 实现扣减库存的途径

因为 Redis 的并发量比 MySQL 要高,可以引用 Redis 来优化库存扣减的逻辑,从而提 高系统的吞吐量

设置库存, 创建或维护时将数据写入 Redis 缓存

redisClient.set("prdt_id_stock", 10);

扣减库存

  1. Lua 脚本完成原子操作

    if (redis.call('exists', KEYS[1]) == 1) then
       local stock = tonumber(redis.call('get', KEYS[1]));
       if (stock == -1) then
          return 1;
       end;
       if (stock > 0) then
          redis.call('incrby', KEYS[1], -1);
          return stock;
       end;
       return 0;
    end;
    return -1;
    
  2. Java API 的操作

    redisClient.decr("prdt_id_stock");
    

6 数据一致性的讨论

目前记录活动的数量出现在三个地方

  1. 活动表 – 售卖数量 static
  2. 库存表 – 库存数量 dynamic
  3. Redis – 库存数量 dynamic

问题是如何保证 库存表Redis 的库存数量和 活动表 的数量一致

  1. 在分布式系统中要不保证强一致性是不可能的
  2. 通过 BASE 理论,在互联网业务中只是要求最终一致性即可

在保证数据最终一致的方式上,这里引用了消息队列的机制,常见的消息队列包括

  1. 消息队列的好处是可以慢慢出来请求,不必同步处理相应结果
  2. 除了异步任务之外,一般还用于处理失败的情况下重试处理,重复消费直到成功
  3. 目前主流的消息队列包括 RocketMQ, Kafka

    • 一般使用 Broker 的模型, 所有的消息需要过 Broker 进行处理
    [Producer] --- send --> [Broker] -- receive --> [Consumer]
    
    • 如果 Broker 可能会重复发送, 消费者在做 API 时需要实现接口的幂等性

7 常见处理问题

7.1 倒计时实现方式

  1. 客户单第一次获取服务端的时间
  2. 客户端使用本地时间进行倒计时的操作

7.2 下单和减库存的细节

  1. 先下单锁定库存 (需要设置超时释放库存)
  2. 支付减库存

7.3 如何防止超卖

  1. 将数据放入 Redis 的缓存中
  2. 利用 Redis 单线程操作,每次只能一个线程操作库存

7.4 库存写回数据库时机

使用定时任务, 每秒回写一下库存

7.5 双十一刷爆页面的处理策略

  1. CDN 来缓存 (首推这种策略)
  2. 使用限流 (Rate Limiter), 页面返回系统繁忙
  3. 增加页面访问验证码

7.6 针对爬虫和黄牛恶意攻击的抵御策略

  1. 增加页面访问验证码, 需要人工识别并输入验证码
  2. 增加 IP 黑名单, 在黑名单中直接拒绝

7.7 如果秒杀服务挂了,如果不影响正常服务

  1. 使用熔断策略
  2. Hystrix
  3. Sentinel

Last Updated 2021-08-06 Fri 20:12. Created by Jinghui Hu at 2021-08-06 Fri 12:22.