秒杀系统如何设计?

  对于秒杀,相信大家都不陌生。秒杀场景经常都会出现在电商网站的促销活动中或者12306抢票中。简单来说,秒杀就是在同一个时间大量请求争抢购买同一商品并完成交易的过程。虽说秒杀只是一个活动,但对技术要求可不低,从架构视角来看,秒杀系统本质是一个高性能、高一致、高可用的三高系统,而在这高并发下应如何设计秒杀系统?下面我们一起来看看需要注意哪些细节。

1、瞬时高并发

  秒杀活动一般都是在秒杀时间的前几分钟,用户请求会突然增加,达到秒杀时间点时,并发量会达到顶峰。因为这类活动是大量用户抢少量商品的场景,必定会出现狼多肉少的情况,所以其实绝大部分用户秒杀会失败,只有极少部分用户能够成功。正常情况下,大部分用户会看到商品已抢光的提醒,用户看到该提醒后大概率不会停留在活动页面,如此一来,用户并发量又会急剧下降。所以这个峰值的持续时间其实是非常短的,这样就会出现瞬时高并发的情况,下面用一张图直观的感受一下流量的变化:

  由图可看出,秒杀系统的并发量存在瞬时凸峰的特点,也叫做流量突刺现象。像这种瞬时高并发的场景,传统的系统很难应付,我们需要设计一套全新的系统,可从以下几个方面入手:

(1)CDN加速的页面静态化;

(2)缓存;

(3)MQ异步处理;

(4)限流;

(5)分布式锁。

2、页面静态化

  活动页面是并发量最大的地方,因为它是用户流量的第一入口,如果这些流量能直接访问服务端,估计服务端会因为承受不住这么大流量压力而直接崩掉。活动页面大部分内容是固定的,如商品名称、商品描述、图片等。为减少不必要的服务端请求,一般会对活动页面做静态化处理。用户浏览商品等常规操作不会请求到服务端。只有到了秒杀时间点且用户点了秒杀按钮才允许访问服务端,这样处理能过滤掉绝大多数的无效请求。

  但只做页面静态化是不够的,因为用户分布在全国各地,地域相差很远,网速也各不相同。要如何才能让用户最快访问到活动页面呢?这就需要使用CDN(即内容分发网络),使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。

3、秒杀按钮

    很多用户怕错过秒杀时间点,所以都会提前进入到活动页面,此时的秒杀按钮是置灰不可点击的,只有到了秒杀时间点,秒杀按钮才允许点击。在当时很多用户是迫不及待的,大家都会通过不停刷新页面来争取第一时间去点击秒杀按钮。从上面得知,该活动页面是静态的,那么在静态页面中如何控制秒杀按钮,只有在秒杀时间才能点亮呢?

    这里就是通过JS文件来控制的,在活动页面中加入一个JS文件引用,该JS文件中包含秒杀开始标志为否,当秒杀开始的时候生成一个新的JS文件(文件名保持不变,只是内容不一样),更新秒杀开始标志为是,加入下单页面的URL及随机数参数(这个随机数只会产生一个,即所有人看到的URL都是同一个,服务器端可以用redis这种分布式缓存服务器来保存随机数),并被用户浏览器加载,控制秒杀商品页面的展示。这个JS文件的加载可以加上随机版本号(例如xx.js?v=35623823),这样就不会被浏览器、CDN和反向代理服务器缓存。这个JS文件非常小,即使每次浏览器刷新都访问JS文件服务器也不会对服务器集群和网络带宽造成太大压力。

4、MQ异步处理

  真实的秒杀场景中有三个核心流程:秒杀-->下单-->支付。而在这流程中,真正并发量大的则是秒杀功能,下单和支付功能的并发量实际很小。所以在设计秒杀系统时,就有必要把下单和支付功能从秒杀的主流程中拆分开来,特别是下单功能要做成MQ异步处理的,而支付功能,就好比支付宝支付,是业务场景本身保证的异步。则秒杀后下单的流程如下:

如果使用MQ,则需要关注以下几个问题:

(1)消息丢失问题

  秒杀成功后往MQ发送下单消息时,有可能会失败,造成的原因有很多,如网络问题,MQ服务端磁盘问题、broker挂了等,这些原因都可能造成消息丢失。

  为了防止消息丢失,我们可以加一张消息发送表,在生产者发送MQ消息之前,先把该条消息写入消息发送表,初始状态是待处理,然后再发送MQ消息。消费者消费消息时,处理完业务逻辑之后,再回调生产者的一个接口,修改消息状态为已处理。如果生产者把消息写入消息发送表之后,再发送MQ消息到MQ服务端的过程中失败了,也会造成了消息丢失。所以在这里可以使用job,增加重试机制,用job每隔一段时间去查询消息发送表中状态为待处理的数据,然后重新发送MQ消息。

(2)重复消费问题

  如果网络超时,本身就可能会有消费重复的消息。但由于消息发送者增加了重试机制,就会导致消费者重复消息的概率增大。为了解决重复消息问题,我们可以加一张消息处理表,消费者读到消息之后,先判断一下消息处理表是否存在该消息,如果存在,表示是重复消费,就直接返回。如果不存在,则进行下单操作,接着将该消息写入消息处理表中,再返回。在这里有个关键的点就是下单和写消息处理表,要放在同一个事务中,保证原子操作。

(3)垃圾消息问题

  如果因某些原因,消息消费者下单一直失败,一直不能回调状态变更接口,这样job会不停的重试发消息,最后会产生大量的垃圾消息。所以每次在job重试时,需要先判断一下消息发送表中该消息的发送次数是否达到最大限制,如果达到了就直接返回。如果没有达到就将次数加1,然后发送消息。这样如果出现异常,只会产生少量的垃圾消息,不会影响到正常的业务。

(4)延迟消费问题

  一般情况下,如果用户秒杀成功了,下单之后在15分钟之内还未完成支付的话,该订单会被自动取消,回退库存。那么在15分钟内未完成支付,订单被自动取消的功能,要如何实现呢?

  我们首先想到的可能是job,因为它比较简单。但job有个问题,需要每隔一段时间处理一次,实时性不太好,所以就可以使用延迟队列。我们都知道rocketmq自带了延迟队列的功能。下单时消息生产者会先生成订单,此时状态为待支付,然后会向延迟队列中发一条消息。达到了延迟时间,消息消费者读取消息之后,会查询该订单的状态是否为待支付。如果是待支付状态,则会更新订单状态为取消状态。如果不是待支付状态,说明该订单已经支付过了,则直接返回。还有个关键点,用户完成支付之后,会修改订单状态为已支付。

5、如何限流

  在秒杀活动中,如果我们运气好,则会用很低的价格买到非常不错的商品。但有些高手可能会使用一些技术,在自己的服务器上模拟正常用户登录系统,跳过秒杀页面直接调用秒杀接口。一般我们手动操作时一秒钟只能点击一次秒杀按钮,但如果是服务器,那么一秒钟可以请求上千次接口。如果不做任何限制,那绝大部分商品可能会被机器抢到,而非正常用户,这样是不是就不太公平了,所以为了识别这些非法请求,我们需要做一些限制。

目前有两种常用的限流方式:

➢基于nginx限流

➢基于redis限流

  秒杀最终的本质是数据库的更新,但是有很多大量无效的请求,我们最终要做的就是如何把这些无效的请求过滤掉,防止渗透到数据库。限流的话,还可以从这些方面入手:

(1)前端限流:用户在秒杀按钮点击后发起请求,那么在接下来的几秒内是无法点击(通过设置按钮为disable)。这样可以防止用户疯狂点击请求。

(2)对接口限流:限制请求的接口总次数,这种限制对于系统的稳定性是非常有必要的。但可能由于有些非法请求次数太多,达到了该接口的请求上限,而影响其他的正常用户访问该接口,这样看起来就有点得不偿失。

(3)对同一用户限流:为了防止某个用户请求接口次数过于频繁,可以只针对该用户做限制,比如限制同一个用户id每分钟只能请求5次接口。

(4)对同一ip限流:有时候只对某个用户限流是不够的,有些高手可以模拟多个用户请求,这时需要用nginx加同一ip限流功能。

(5)加验证码:一般情况下,用户在请求之前,需要先输入验证码。用户发起请求之后,服务端会去校验该验证码是否正确。只有正确才允许进行下一步操作,否则直接返回,并且提示验证码错误。此外,验证码一般是一次性的,同一个验证码只允许使用一次,不允许重复使用。

6、缓存问题

  通常情况下,我们需要在redis中保存商品信息,其包含:商品id、商品名称、规格属性、库存等信息,同时数据库中也要有相关信息,毕竟缓存不完全可靠。用户在点击秒杀按钮,请求秒杀接口的过程中,需要传入的商品id参数,然后服务端需要校验该商品是否合法,再根据商品id,先从缓存中查询商品,如果商品存在,则参与秒杀。如果不存在,则需要从数据库中查询商品,如果存在,则将商品信息放入缓存,然后参与秒杀。如果商品不存在,则直接提示失败。这样看来没有什么问题,但是深入分析会发现一些问题,如下:

(1)缓存穿透:指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库。针对这类问题,我们可以对这些不存在的数据缓存一个空数据,或者着类请求进行过滤来解决。

(2)缓存雪崩:指的是由于数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机,导致大量的请求都到达数据库。在有缓存的系统中,系统非常依赖于缓存,缓存分担了很大一部分的数据请求。当发生缓存雪崩时,数据库无法处理这么大的请求,导致数据库崩溃。为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现;而针对缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用。也可以进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩。

(3)缓存一致性:是要求数据更新的同时缓存数据也能够实时更新。针对缓存一致问题,我们可以在数据更新的同时立即去更新缓存,或者在读缓存之前先判断缓存是否是最新的,如果不是最新的先进行更新。因为保证缓存一致性需要付出很大的代价,缓存数据最好是那些对一致性要求不高的数据,允许缓存数据存在一些脏数据。

7、分布式锁

  如果在高并发下,有大量的请求都去查库存,会造成库存脏数据,所以必须得加分布式锁。redis分布式锁的原理非常简单,就是在运行实际的业务代码之前,首先到redis中去获得唯一的redis锁,如果获取到,则继续执行业务代码,并在业务代码结束后主动释放锁;若未成功获取到锁,则不执行业务代码,核心代码如下:

通过以上代码可以看出,所谓的“获取锁”,“释放锁”操作,本质上就是向redis服务器插入和删除键值对。

8、读多写少

  在秒杀的过程中,系统通常会先查一下库存是否足够,如果足够才允许下单,写数据库。如果不够则直接返回该商品已经抢光。由于大量用户抢少量商品,只有极少部分用户能够抢成功,所以绝大部分用户在秒杀时,库存其实是不足的,系统会直接返回该商品已经抢光。这是非常典型的:读多写少的场景。

  如果有数十万的请求过来,同时通过数据库查缓存是否足够,此时数据库可能会挂掉。因为数据库的连接资源非常有限,比如:mysql,无法同时支持这么多的连接。这时就应该改用缓存,比如:redis。不过即便用了redis,也需要部署多个节点。

9、库存问题

  对于库存问题,真正的秒杀商品的场景,不是说扣完库存,就完事了,如果用户在一段时间内还没有完成支付,扣减的库存还是要加回去的。

(1)redis预减库存

  很多请求进来都需要后台查询库存,这是一个频繁读的场景,可以使用Redis来预减库存,在秒杀开始前可以在redis设值,比如redis.set(goodsId,100),这里预放的库存为100可以设值为常量,每次下单成功之后,Integer stock = (Integer)redis.get(goosId); 然后判断sock的值,如果小于常量值就减去1;不过要注意,当取消的时候需要增加库存,增加库存的时候也得注意不能大于之间设定的总库存数(查询库存和扣减库存需要原子操作,此时可以借助lua脚本)下次下单再获取库存的时候,直接从redis里面查就可以了。

(2)库存超卖问题

  在下单扣减库存的业务场景中,需要保证大流量、高并发下商品的库存不能为负。

免责声明:本账号部分分享的资料来自网络收集和整理,所有文字和图片版权归属于原作者所有,文章仅供读者学习交流使用,并请自行核实相关内容,如文章内容涉及侵权,请联系后台管理员删除。
本文来源博客园,作者:苏三说技术,https://www.cnblogs.com/12lisu/p/15154397.html

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值