设计秒杀系统
介绍
纯粹为了熟悉系统设计而搭建的项目。
- 场景没什么可说的就是减库存下单,流程很简单;
- 秒杀会在短时间内产生大量流量,会有很多用户“疯狂”下单,但是又只有少部分用户能够真正秒杀成功;
- 主要关注如何设计系统的高可用和高性能特性。
系统分析
按照场景、服务、存储、升级四个层次来分析秒杀系统的设计。
场景
- 瞬时大流量高并发
从QPS来讲,每秒1000人访问,秒杀时每秒数十万人访问,QPS增加100倍以上。 - 有限库存,不能超卖
- 防止黄牛的恶意请求
一般黄牛请求都是有规律的,能够制定一些规则来屏蔽黄牛的非法请求。
服务
- 秒杀服务
- 商品信息和库存服务
- 订单服务
- 支付服务
存储
商品信息表
1 | create table product ( |
秒杀活动表
1 | create table seckill ( |
库存信息表
1 | create table stock ( |
订单信息表
1 | create table order ( |
为了分析如何加索引,先来看下需要实现的有哪些功能。
商家侧
- 商家查询商品信息
- 商家创建秒杀活动
- 商家创建库存
用户侧
- 用户查询秒杀活动、商品、库存
- 用户下单
- 用户更新库存
1
2
3
4
5
6-- 查询库存余量
select number from stock
where product_id = 123 and seckill_id = 456;
-- 扣减库存
update stock set stock = stock - 1
where product_id = 123 and seckill_id = 456;
库存扣减时机
- 下单时立即扣减库存
优点:用户体验最好,利用数据库锁机制一定能够精准扣减库存,用户一定能支付成功;
缺点:可能被恶意下单,下单后不付款,别人也买不了了。 - 先下单,不减库存,实际支付后扣减库存
优点:可以避免恶意下单;
缺点:对用户体验差,可能下单成功但是无法付款 - 下单后锁定库存,支付成功后再扣减库存
限购
限制一个用户只能秒杀一次。
扣减库存后将用户ID存入Redis集合中,之后用户下单时需要校验用户ID是否存在于Redis集合中。
如果用户支付失败,需要再删除集合内的用户ID。
解决超卖问题
库存最终是通过数据库存储的。
- 加行锁
开启事务,并在查询库存时加行锁。
缺点:加行锁性能慢1
2
3
4
5
6
7start transaction;
-- 查询库存余量
select number from stock
where product_id = 123 and seckill_id = 456
for update;
-- 扣减库存
...没有变化 - 使用update语句自带的行锁
只有库存大于0的时候才会进行更新操作,因为update语句本身是带行锁的、当前读的。
缺点:还是对MySQL依赖比较大,如果MySQL崩溃服务就不可用了。
解决办法:在秒杀活动中,大部分请求都是失败的,可以利用Redis来过滤这些请求。1
2
3
4
5
6-- 查询库存余量
...没有变化
-- 扣减库存
update stock set stock = stock - 1
where product_id = 123 and seckill_id = 456
and number > 0;
库存预热
初步设计中,使用数据库保存库存,面临的主要问题是MySQL无法应对秒杀的瞬时大量请求。
秒杀之前将商品信息缓存起来,减少数据库本身的流量,在下单前校验库存,只有检查成功的请求才能进入到下游。
数据库中的库存是通过行锁来保证并发更新安全的,但是数据库中的库存数据怎么和缓存保持一致呢?
MySQL扣减库存成功或失败都会影响Redis中前置库存(用于锁定)的值,所以在扣减操作后需要更新Redis中库存的值。
削峰限流
MQ堆积订单,下游Consumer根据自己的消费能力来处理下单请求。
其他优化
- 数据库层面
分库分表 - 缓存层面
- 可拓展
服务的可扩展,可以水平添加机器将用户请求分担到不同的机器上去。数据库可扩展,支持分库分表,对于用户的请求,映射到不同的数据库,减少单台数据库的压力。
扩展
如果Redis不足以应对流量
上面的设计中,库存锁定依赖Redis,虽然Redis性能高过MySQL,但是能力仍然有限,那么如果流量超过了Redis的承载能力怎么办?
- Redis库存扣减完毕后,后面请求可以直接拒绝
- 限流
秒杀服务器挂掉
尽量不要影响其他服务,加入熔断机制。
流量过滤
库存预热用Redis来过滤流量,大大减轻了MySQL压力(MySQL最多支持1KQPS,Redis可支持10WQPS),其他流量过滤措施如:
- 页面资源静态化,可使用CDN提速
- 禁止重复提交(前端限流):秒杀开始之后,可以对用户点击后响应前按钮置灰。
秒杀前防刷
防止商品详情页被刷爆:
- 未开始抢购时,禁用抢购按钮
比如轮询服务器时间,获取距离活动开始时间的时间差,轮询主要是为了不断纠正客户端时间,和服务端的时间保持一致;
防刷(脚本、爬虫)
- 刷单问题
有的用户会频繁调接口来达到刷单的目的,一方面刷接口对系统压力比较大,另一方面同一用户可能会占用大量的库存资源
解决办法是限制一个用户调用的频率,比如一个用户一分钟只能调10次,超过这个时间就提示用户访问过于频繁、或弹出验证码让用户输入。
防刷功能可以通过在Redis中计数来实现,记<userId, count>
,使用lua脚本实现原子操作。 - 多账号刷单问题
有的用户为了避免被上述规则拦住,就会注册多个账号来刷单。
解决办法是拦IP,但是如果多个用户有相同的出口IP,就会导致误拦的情况,解决办法是加白名单,比如阿里云盾就是采用的这种方式:通过设置白名单解决因误判IP被拦截问题。
以上的两种维度(用户userId、IP)都可以通过黑名单来实现。
秒杀系统的变种
抢票(火车票、机票)
和秒杀系统的区别是:根据区段锁定库存。