1. 前后分离
- 活动页面绝大多数内容是固定的,比如:商品名称、商品描述、图片等。为了减少不必要的服务端请求,通常情况下,会对活动页面做
静态化
处理。 CDN
2. 库存缓存
如果有数十万的请求过来,同时通过数据库查缓存是否足够,此时数据库可能会挂掉。因为数据库的连接资源非常有限,比如:mysql
,无法同时支持这么多的连接。而应该改用缓存,比如:redis
。
2.1 缓存击穿
在高并发下,同一时刻会有大量的请求,都在秒杀同一件商品,这些请求同时去查缓存中没有数据,然后又同时访问数据库。结果悲剧了,数据库可能扛不住压力,直接挂掉。
如何解决这个问题呢?
这就需要加锁,最好使用分布式锁
。
当然,针对这种情况,最好在项目启动之前,先把缓存进行预热
。即事先把所有的商品,同步到缓存中,这样商品基本都能直接从缓存中获取到,就不会出现缓存击穿的问题了。
是不是上面加锁这一步可以不需要了?
表面上看起来,确实可以不需要。但如果缓存中设置的过期时间不对,缓存提前过期了,或者缓存被不小心删除了,如果不加速同样可能出现缓存击穿。
其实这里加锁,相当于买了一份保险。
2.2 缓存穿透
如果有大量的请求传入的商品id,在缓存中和数据库中都不存在,这些请求不就每次都会穿透过缓存,而直接访问数据库了。
由于前面已经加了锁,所以即使这里的并发量很大,也不会导致数据库直接挂掉。
但很显然这些请求的处理性能并不好,有没有更好的解决方案?
这时可以想到布隆过滤器
。
秒杀开始前,将所有商品id初始化到布隆过滤器,不存在需要更新布隆过滤器的情况,如果前端请求传来不存在的id的,那么请求大概率会被布隆过滤器拦截(被布隆过滤器命中的id不一定是真的存在,但是没命中的id一定不存在)。
3. 库存问题
真正的秒杀商品的场景,不是说扣完库存,就完事了,如果用户在一段时间内,还没完成支付,扣减的库存是要加回去的。
所以,在这里引出了一个预扣库存
的概念,预扣库存的主要流程如下:
扣减库存中除了上面说到的预扣库存
和回退库存
之外,还需要特别注意的是库存不足和库存超卖问题。
3.1 预扣库存
虽然前面我们我做了分布式锁、缓存等措施,防止过多查询请求直接去到数据库,但是这些措施都是针对查询请求的,扣减库存的时候仍然可能有大量请求并发去到数据库,例如:同时有10万个请求查询缓存得到当前库存量不等于0,它们接下来全都要去数据库扣减库存。
update product set stock=stock-1 where id=product and stock > 0;
上面的sql有点类似数据库乐观锁的思想,保证不会超卖。
这里我们可以采取限流
措施(可以用sentinel等),例如:虽然有10万个请求将要扣减库存,但是我们只分配1000个数据库扣减名额(sentinel令牌桶🪣),甚至都不用分配1000个名额,因为秒杀商品数量肯定是已知的,假设秒杀商品数量为10个,那我们只需要放行10个扣减库存的请求去到数据库即可。
但是,还有一类情况,就是参与秒杀的商品数量确实很多(比如小米手机刚面世的时候经常搞饥饿营销,几百万甚至几千万的请求去抢几十万的手机😔),远大于数据库能承受的并发能力。
这里我们可以采取 限流
+ 延迟重试
,例如:我们的秒杀商品有1万个,100万个请求要去扣减库存,但是数据库只能承受1000个并发,那我们就限流每秒只有1000个请求去到数据库,让剩下的请求延迟50毫秒再重试,如果业务能允许用户一直等待秒杀结果,那就一直循环重试到有资格去数据库扣减库存或者1万个秒杀商品被抢完,抢购时间可能持续几分钟甚至更久,让用户一直等待结果也不合理,这时我们循环重试一定次数后如果还没有资格,那就先响应这个请求,并且把这个请求扔到延迟队列继续重试。
这里为什么不直接响应没获取到数据库扣减库存资格的请求呢?为什么不让其稍后再试呢?这主要是考虑到
秒杀公平
的问题,例如:小明在第1秒发起秒杀请求,但是因为秒杀系统数据库并发能力不够,你让小明稍后重试,但是注意这个时候秒杀的商品是足够的,第2秒小王发起秒杀并成功,第3秒小明再次秒杀发现商品已经被抢光,这对小明来说明显不公平的。
3.2 回退库存
通常情况下,如果用户秒杀成功了,下单之后,在15分钟之内还未完成支付的话,该订单会被自动取消,回退库存。
那么,在15分钟内未完成支付,订单被自动取消的功能,要如何实现呢?
我们首先想到的可能是job,因为它比较简单。
但job有个问题,需要每隔一段时间处理一次,实时性不太好。
还有更好的方案?
答:使用延迟队列
。
我们都知道rocketmq,自带了延迟队列的功能。
下单时消息生产者会先生成订单,此时状态为待支付,然后会向延迟队列中发一条消息。达到了延迟时间,消息消费者读取消息之后,会查询该订单的状态是否为待支付。如果是待支付状态,则会更新订单状态为取消状态。如果不是待支付状态,说明该订单已经支付过了,则直接返回。
当然需要完成上述功能,需要用户完成支付之后,修改订单状态为已支付。具体来说,支付系统完成支付之后,回调秒杀系统这边提供的回调接口
去修改订单支付状态。
4. 接口限流
通过秒杀活动,如果我们运气爆棚,可能会用非常低的价格买到不错的商品(这种概率堪比买福利彩票中大奖)。
但有些高手,并不会像我们一样老老实实,通过秒杀页面点击秒杀按钮,抢购商品。他们可能在自己的服务器上,模拟正常用户登录系统,跳过秒杀页面,直接调用秒杀接口。
如果是我们手动操作,一般情况下,一秒钟只能点击一次秒杀按钮。
但是如果是服务器,一秒钟可以请求成上千接口。
这种差距实在太明显了,如果不做任何限制,绝大部分商品可能是被机器抢到,而非正常的用户,有点不太公平。
所以,我们有必要识别这些非法请求,做一些限制。那么,我们该如何现在这些非法请求呢?
目前有两种常用的限流方式:
- 基于nginx限流
- 基于redis限流
4.1 对同一用户限流
限制同一个用户id,比如每分钟只能请求5次接口。
4.2 对同一ip限流
有时候只对某个用户限流是不够的,有些高手可以模拟多个用户请求,这种nginx就没法识别了。
这时需要加同一ip限流功能。限制同一个ip,比如每分钟只能请求5次接口。
但这种限流方式可能会有误杀的情况,比如同一个公司或网吧的出口ip是相同的,如果里面有多个正常用户同时发起请求,有些用户可能会被限制住。
4.3 对接口限流
别以为限制了用户和ip就万事大吉,有些高手甚至可以使用代理,每次都请求都换一个ip。
这时可以限制请求的接口总次数。
在高并发场景下,这种限制对于系统的稳定性是非常有必要的。但可能由于有些非法请求次数太多,达到了该接口的请求上限,而影响其他的正常用户访问该接口。看起来有点得不偿失。
4.4 加验证码
相对于上面三种方式,加验证码的方式可能更精准一些,同样能限制用户的访问频次,但好处是不会存在误杀的情况。
通常情况下,用户在请求之前,需要先输入验证码。用户发起请求之后,服务端会去校验该验证码是否正确。只有正确才允许进行下一步操作,否则直接返回,并且提示验证码错误。
4.5 提高业务门槛
上面说的加验证码虽然可以限制非法用户请求,但是有些影响用户体验。用户点击秒杀按钮前,还要先输入验证码,流程显得有点繁琐,秒杀功能的流程不是应该越简单越好吗?
其实,有时候达到某个目的,不一定非要通过技术手段,通过业务手段也一样。
我们通过提高业务门槛,比如只有会员才能参与秒杀活动,普通注册用户没有权限。或者,只有等级到达3级以上的普通用户,才有资格参加该活动。
评论