2024年11月秒杀品购物(适合做秒杀的产品)

发布时间:

  ⑴高并发下如何设计秒杀系统?这是一个高频面试题。这个问题看似简单,但是里面的水很深,它考查的是高并发场景下,从前端到后端多方面的知识。

  ⑵秒杀一般出现在商城的促销活动中,指定了一定数量(比如:个)的商品(比如:手机),以极低的价格(比如:.元),让大量用户参与活动,但只有极少数用户能够购买成功。这类活动商家绝大部分是不赚钱的,说白了是找个噱头宣传自己。

  ⑶虽说秒杀只是一个促销活动,但对技术要求不低。下面给大家总结一下设计秒杀系统需要注意的个细节。

  ⑷一般在秒杀时间点(比如:点)前几分钟,用户并发量才真正突增,达到秒杀时间点时,并发量会达到顶峰。

  ⑸但由于这类活动是大量用户抢少量商品的场景,必定会出现狼多肉少的情况,所以其实绝大部分用户秒杀会失败,只有极少部分用户能够成功。

  ⑹正常情况下,大部分用户会收到商品已经抢完的提醒,收到该提醒后,他们大概率不会在那个活动页面停留了,如此一来,用户并发量又会急剧下降。所以这个峰值持续的时间其实是非常短的,这样就会出现瞬时高并发的情况,下面用一张图直观地感受一下流量的变化:

  ⑺像这种瞬时高并发的场景,传统的系统很难应对,我们需要设计一套全新的系统。可以从以下几个方面入手:

  ⑻活动页面是用户流量的第一入口,所以是并发量最大的地方。

  ⑼如果这些流量都能直接访问服务端,恐怕服务端会因为承受不住这么大的压力,而直接挂掉。

  ⑽通常情况下,我们需要在redis中保存商品信息,里面包含:商品id、商品名称、规格属性、库存等信息,同时数据库中也要有相关信息,毕竟缓存并不完全可靠。

  ⑾用户在点击秒杀按钮,请求秒杀接口的过程中,需要传入的商品id参数,然后服务端需要校验该商品是否合法。

  ⑿大致流程如下图所示:

  ⒀根据商品id,先从缓存中查询商品,如果商品存在,则参与秒杀。如果不存在,则需要从数据库中查询商品,如果存在,则将商品信息放入缓存,然后参与秒杀。如果商品不存在,则直接提示失败。

  ⒁这个过程表面上看起来是OK的,但是如果深入分析一下会发现一些问题。

  ⒂比如商品A第一次秒杀时,缓存中是没有数据的,但数据库中有。虽说上面有如果从数据库中查到数据,则放入缓存的逻辑。

  ⒃然而,在高并发下,同一时刻会有大量的请求,都在秒杀同一件商品,这些请求同时去查缓存中没有数据,然后又同时访问数据库。结果悲剧了,数据库可能扛不住压力,直接挂掉。

  ⒄如何解决这个问题呢?

  ⒅这就需要加锁,最好使用分布式锁。

  ⒆扣减库存中除了上面说到的预扣库存和回退库存之外,还需要特别注意的是库存不足和库存超卖问题。

  ⒇使用数据库扣减库存,是最简单的实现方案了,假设扣减库存的sql如下:

  ⒈这种写法对于扣减库存是没有问题的,但如何控制库存不足的情况下,不让用户操作呢?

  ⒉这就需要在update之前,先查一下库存是否足够了。

  ⒊大家有没有发现这段代码的问题?

  ⒋没错,查询操作和更新操作不是原子性的,会导致在并发的场景下,出现库存超卖的情况。

  ⒌有人可能会说,这样好办,加把锁,不就搞定了,比如使用synchronized关键字。

  ⒍确实,可以,但是性能不够好。

  ⒎还有更优雅的处理方案,即基于数据库的乐观锁,这样会少一次数据库查询,而且能够天然地保证数据操作的原子性。

  ⒏只需将上面的sql稍微调整一下:

  ⒐在sql最后加上:stock > ,就能保证不会出现超卖的情况。

  ⒑但需要频繁访问数据库,我们都知道数据库连接是非常昂贵的资源。在高并发的场景下,可能会造成系统雪崩。而且,容易出现多个请求,同时竞争行锁的情况,造成相互等待,从而出现死锁的问题。

  ⒒redis的incr方法是原子性的,可以用该方法扣减库存。伪代码如下:

  ⒓估计很多小伙伴,一开始都会按这样的思路写代码。但如果仔细想想会发现,这段代码有问题。

  ⒔如果在高并发下,有多个请求同时查询库存,当时都大于。由于查询库存和更新库存非原则操作,则会出现库存为负数的情况,即库存超卖。

  ⒕当然有人可能会说,加个synchronized不就解决问题?

  ⒖调整后代码如下:

  ⒗加synchronized确实能解决库存为负数问题,但是这样会导致接口性能急剧下降,每次查询都需要竞争同一把锁,显然不太合理。

  ⒘为了解决上面的问题,代码优化如下:

  ⒙该代码主要流程如下:

  ⒚该方案咋一看,好像没问题。

  ⒛但如果在高并发场景中,有多个请求同时扣减库存,大多数请求的incrby操作之后,结果都会小于。

  ①虽说,库存出现负数,不会出现超卖的问题。但由于这里是预减库存,如果负数值负得太多的话,后面万一要回退库存时,就会导致库存不准。

  ②那么,有没有更好的方案呢?

  ③我们都知道lua脚本,是能够保证原子性的,它跟redis一起配合使用,能够完美解决上面的问题。

  ④lua脚本有段非常经典的代码:

  ⑤该代码的主要流程如下:

  ⑥之前我提到过,在秒杀的时候,需要先从缓存中查商品是否存在,如果不存在,则会从数据库中查商品。如果数据库中,则将该商品放入缓存中,然后返回。如果数据库中没有,则直接返回失败。

  ⑦大家试想一下,如果在高并发下,有大量的请求都去查一个缓存中不存在的商品,这些请求都会直接打到数据库。数据库由于承受不住压力,而直接挂掉。

  ⑧那么如何解决这个问题呢?

  ⑨这就需要用redis分布式锁了。

  ⑩使用redis的分布式锁,首先想到的是setNx命令。

  Ⅰ用该命令其实可以加锁,但和后面的设置超时时间是分开的,并非原子操作。

  Ⅱ假如加锁成功了,但是设置超时时间失败了,该lockKey就变成永不失效的了。在高并发场景中,该问题会导致非常严重的后果。

  Ⅲ那么,有没有保证原子性的加锁命令呢?

  Ⅳ用redis的set命令,它可以指定多个参数。

  Ⅴ由于该命令只有一步,所以它是原子操作。

  Ⅵ接下来,有些朋友可能会问:在加锁时,既然已经有了lockKey锁标识,为什么要需要记录requestId呢?

  Ⅶ答:requestId是在释放锁的时候用的。

  Ⅷ在释放锁的时候,只能释放自己加的锁,不允许释放别人加的锁。

  Ⅸ这里为什么要用requestId,用userId不行吗?

  Ⅹ答:如果用userId的话,假设本次请求流程走完了,准备删除锁。此时,巧合锁到了过期时间失效了。而另外一个请求,巧合使用的相同userId加锁,会成功。而本次请求删除锁的时候,删除的其实是别人的锁了。

  ㈠当然使用lua脚本也能避免该问题:

  ㈡它能保证查询锁是否存在和删除锁是原子操作。

  ㈢上面的加锁方法看起来好像没有问题,但如果你仔细想想,如果有万的请求同时去竞争那把锁,可能只有一个请求是成功的,其余的个请求都会失败。

  ㈣在秒杀场景下,会有什么问题?

  ㈤答:每万个请求,有个成功。再万个请求,有个成功。如此下去,直到库存不足。这就变成均匀分布的秒杀了,跟我们想象中的不一样。

  ㈥如何解决这个问题呢?

  ㈦答:使用自旋锁。

  ㈧在规定的时间,比如毫秒内,自旋不断尝试加锁,如果成功则直接返回。如果失败,则休眠毫秒,再发起新一轮的尝试。如果到了超时时间,还未加锁成功,则直接返回失败。

  ㈨除了上面的问题之外,使用redis分布式锁,还有锁竞争问题、续期问题、锁重入问题、多个redis实例加锁问题等。

  ㈩这些问题使用redisson可以解决,由于篇幅的原因,在这里先保留一点悬念,有疑问的私聊给我。后面会出一个专题介绍分布式锁,敬请期待。

  我们都知道在真实的秒杀场景中,有三个核心流程:

  通过秒杀活动,如果我们运气爆棚,可能会用非常低的价格买到不错的商品(这种概率堪比买福利彩票中大奖)。

  但有些高手,并不会像我们一样老老实实,通过秒杀页面点击秒杀按钮,抢购商品。他们可能在自己的服务器上,模拟正常用户登录系统,跳过秒杀页面,直接调用秒杀接口。

  如果是我们手动操作,一般情况下,一秒钟只能点击一次秒杀按钮。

  通常情况下,用户在请求之前,需要先输入验证码。用户发起请求之后,服务端会去校验该验证码是否正确。只有正确才允许进行下一步操作,否则直接返回,并且提示验证码错误。

  此外,验证码一般是一次性的,同一个验证码只允许使用一次,不允许重复使用。

  普通验证码,由于生成的数字或者图案比较简单,可能会被破解。优点是生成速度比较快,缺点是有安全隐患。

  还有一个验证码叫做:移动滑块,它生成速度比较慢,但比较安全,是目前各大互联网公司的首选。

  上面说的加验证码虽然可以限制非法用户请求,但是有些影响用户体验。用户点击秒杀按钮前,还要先输入验证码,流程显得有点繁琐,秒杀功能的流程不是应该越简单越好吗?

  其实,有时候达到某个目的,不一定非要通过技术手段,通过业务手段也一样。

  刚开始的时候,全国人民都在同一时刻抢火车票,由于并发量太大,系统经常挂。后来,重构优化之后,将购买周期放长了,可以提前天购买火车票,并且可以在点、、点、点等整点购买火车票。调整业务之后(当然技术也有很多调整),将之前集中的请求,分散开了,一下子降低了用户并发量。

  回到这里,我们通过提高业务门槛,比如只有会员才能参与秒杀活动,普通注册用户没有权限。或者,只有等级到达级以上的普通用户,才有资格参加该活动。