设计一个秒杀系统
所谓“秒杀”,就是网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。
秒杀应该考虑哪些问题
超卖问题
分析秒杀的业务场景,最重要的有一点就是超卖问题,假如备货只有100个,但是最终超卖了200,一般来讲秒杀系统的价格都比较低,如果超卖将严重影响公司的财产利益,因此首当其冲的就是解决商品的超卖问题。
高并发
秒杀具有时间短、并发量大的特点,秒杀持续时间只有几分钟,而一般公司都为了制造轰动效应,会以极低的价格来吸引用户,因此参与抢购的用户会非常的多。短时间内会有大量请求涌进来,后端如何防止并发过高造成缓存击穿或者失效,击垮数据库都是需要考虑的问题。
接口防刷
现在的秒杀大多都会出来针对秒杀对应的软件,这类软件会模拟不断向后台服务器发起请求,一秒几百次都是很常见的,如何防止这类软件的重复无效请求,防止不断发起的请求也是需要我们针对性考虑的
秒杀url
对于普通用户来讲,看到的只是一个比较简单的秒杀页面,在未达到规定时间,秒杀按钮是灰色的,一旦到达规定时间,灰色按钮变成可点击状态。这部分是针对小白用户的,如果是稍微有点电脑功底的用户,会通过F12看浏览器的network看到秒杀的url,通过特定软件去请求也可以实现秒杀。或者提前知道秒杀url的人,一请求就直接实现秒杀了。这个问题我们需要考虑解决
数据库设计
秒杀有把我们服务器击垮的风险,如果让它与我们的其他业务使用在同一个数据库中,耦合在一起,就很有可能牵连和影响其他的业务。如何防止这类问题发生,就算秒杀发生了宕机、服务器卡死问题,也应该让他尽量不影响线上正常进行的业务
大量请求问题
按照1.2的考虑,就算使用缓存还是不足以应对短时间的高并发的流量的冲击。如何承载这样巨大的访问量,同时提供稳定低时延的服务保证,是需要面对的一大挑战。我们来算一笔账,假如使用的是redis缓存,单台redis服务器可承受的QPS大概是4W左右,如果一个秒杀吸引的用户量足够多的话,单QPS可能达到几十万,单体redis还是不足以支撑如此巨大的请求量。缓存会被击穿,直接渗透到DB,从而击垮mysql.后台会将会大量报错
- order_id
- user_id
- Product_id
- State 1已支付 2 未支付
- create_time
id
id | |
product_id | |
Product_name | |
stock | 库存 |
version | |
start_time | |
end_time |
瞬时高并发的场景
- 页面静态化
- CDN加速
- 秒杀按钮置灰
- 读多写少改用缓存,比如:redis。
- 缓存击穿,分布式锁,针对这种情况,最好在项目启动之前,先把缓存进行预热。
- 缓存穿透,就需要把不存在的商品id也缓存起来。需要特别注意的是,这种特殊缓存设置的超时时间应该尽量短一点。不然容易占用过多内存,最好通过布隆过滤器过滤一次。
mq 异步处理 异步下单
真实的秒杀场景中,有三个核心流程:秒杀、下单、支付。真正并发量大的是秒杀功能,下单和支付功能实际并发量很小。所以,我们在设计秒杀系统时,有必要把下单和支付功能从秒杀的主流程中拆分出来,特别是下单功能要做成mq异步处理的。而支付功能,比如支付宝支付,是业务场景本身保证的异步。
防止消息丢失
- 加一张消息发送表。(也可解决重复消费问题)
- 事务消息
重复消费问题,加一张消息发送表,下单和写消息处理表,要放在同一个事务中,保证原子操作。
延迟消费问题,通常情况下,如果用户秒杀成功了,下单之后,在15分钟之内还未完成支付的话,该订单会被自动取消,回退库存。使用延迟队列。rocketmq,自带了延迟队列的功能。
限流、降级、熔断、隔离
这个为啥要做呢,不怕一万就怕万一,万一你真的顶不住了
-
限流,顶不住就挡一部分出去但是不能说不行
-
降级,降级了还是被打挂了
-
熔断,至少不要影响别的系统
-
隔离,你本身就独立的,但是你会调用其他的系统嘛,你快不行了你别拖累兄弟们啊。
-
基于nginx限流
-
基于redis限流
一种验证码叫做:移动滑块,它生成速度比较慢,但比较安全,是目前各大互联网公司的首选。
- 对同一用户限流
- ip 限流
- 接口限流
- 加验证码
限流这里我觉得应该分为前端限流和后端限流。
前端限流:这个很简单,一般秒杀不会让你一直点的,一般都是点击一下或者两下然后几秒之后才可以继续点击,这也是保护服务器的一种手段。
后端限流:秒杀的时候肯定是涉及到后续的订单生成和支付等操作,但是都只是成功的幸运儿才会走到那一步,那一旦100个产品卖光了,return了一个false,前端直接秒杀结束,然后你后端也关闭后续无效请求的介入了。
Tip:真正的限流还会有限流组件的加入例如:阿里的Sentinel、Hystrix等。我这里就不展开了,就说一下物理的限流。
库存问题
扣减库存 –> 创建订单 –> 支付,其他顺序会导致“超卖”的现象
扣减库存,一般需要先查询是否有库存,这2个操作非原子的。可以通过加锁,但更好的是通过数据库乐观锁实现。
|
|
在高并发的场景下,可能会造成系统雪崩。而且,容易出现多个请求,同时竞争行锁的情况,造成相互等待,从而出现死锁的问题。
库存预热
秒杀的本质,就是对库存的抢夺,每个秒杀的用户来你都去数据库查询库存校验库存,然后扣减库存,撇开性能因数,你不觉得这样好繁琐,对业务开发人员都不友好,而且数据库顶不住啊。
redis 扣减库存
redis的incr方法是原子性的,可以用该方法扣减库存。
lua脚本扣减库存
|
|
- 先判断商品id是否存在,如果不存在则直接返回。
- 获取该商品id的库存,判断库存如果是-1,则直接返回,表示不限制库存。
- 如果库存大于0,则扣减库存。
- 如果库存等于0,是直接返回,表示库存不足。
分布式锁
使用redis分布式锁,还有锁竞争问题、续期问题、锁重入问题、多个redis实例加锁问题等。这些问题使用redisson可以解决。
这做法效率低下,不合适做秒杀。简单的方法可以在 Redis 里维护虚拟库存,用原子性减操作来判定是否超卖。