秒杀系统设计
Table of Contents
1 基本参数
- 需求设计一个可以承载 100W QPS 的秒杀系统
- 考虑到 MySQL 的最大 QPS = 1000
- 考虑到 Redis 的最大 QPS = 10W
2 架构设计
在描述活动中涉及到多个微服务的
- ActivityService 活动微服务
- ProductService 产品微服务
- OrderService 订单微服务
- 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 = ???;
注意:
- 在秒杀的情景下乐观锁并不能解决问题
- 高并发情景下可能会把 MySQL 数据库整崩溃
5 Redis 实现扣减库存的途径
因为 Redis 的并发量比 MySQL 要高,可以引用 Redis 来优化库存扣减的逻辑,从而提 高系统的吞吐量
设置库存, 创建或维护时将数据写入 Redis 缓存
redisClient.set("prdt_id_stock", 10);
扣减库存
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;
Java API 的操作
redisClient.decr("prdt_id_stock");
6 数据一致性的讨论
目前记录活动的数量出现在三个地方
- 活动表 – 售卖数量 static
- 库存表 – 库存数量 dynamic
- Redis – 库存数量 dynamic
问题是如何保证 库存表
和 Redis
的库存数量和 活动表
的数量一致
- 在分布式系统中要不保证强一致性是不可能的
- 通过 BASE 理论,在互联网业务中只是要求最终一致性即可
在保证数据最终一致的方式上,这里引用了消息队列的机制,常见的消息队列包括
- 消息队列的好处是可以慢慢出来请求,不必同步处理相应结果
- 除了异步任务之外,一般还用于处理失败的情况下重试处理,重复消费直到成功
目前主流的消息队列包括 RocketMQ, Kafka
- 一般使用 Broker 的模型, 所有的消息需要过 Broker 进行处理
[Producer] --- send --> [Broker] -- receive --> [Consumer]
- 如果 Broker 可能会重复发送, 消费者在做 API 时需要实现接口的幂等性
7 常见处理问题
7.1 倒计时实现方式
- 客户单第一次获取服务端的时间
- 客户端使用本地时间进行倒计时的操作
7.2 下单和减库存的细节
- 先下单锁定库存 (需要设置超时释放库存)
- 支付减库存
7.3 如何防止超卖
- 将数据放入 Redis 的缓存中
- 利用 Redis 单线程操作,每次只能一个线程操作库存
7.4 库存写回数据库时机
使用定时任务, 每秒回写一下库存
7.5 双十一刷爆页面的处理策略
- CDN 来缓存 (首推这种策略)
- 使用限流 (Rate Limiter), 页面返回系统繁忙
- 增加页面访问验证码
7.6 针对爬虫和黄牛恶意攻击的抵御策略
- 增加页面访问验证码, 需要人工识别并输入验证码
- 增加 IP 黑名单, 在黑名单中直接拒绝
7.7 如果秒杀服务挂了,如果不影响正常服务
- 使用熔断策略
- Hystrix
- Sentinel