中间件(四):秒杀系统架构设计

x33g5p2x  于2021-09-28 转载在 其他  
字(4.3k)|赞(0)|评价(0)|浏览(405)

写在前面

最近公司的运营花了很多钱做活动拉新用户,公司APP的日活用户一直在增长 现在已经明显发现每天高峰时间公司搞秒杀活动的时候,比以前有更多的用户在某个时间点蹲守在手机APP前。特价秒杀商品时间一 到,就有大量的并发请求过来,系统压力非常大

1、合理的架构、有限的机器资源优化

因为订单系统目前部署了20台4核8G的机器,整个集群抗每秒上万请求压力是可以的,单台机器500是可以的。即使后续用户量越来越大,大不了就是给订单系统加更多的机器就可以了。

但是这里有一个问题,20台订单系统的机器都是访问同一台机器上部署的MySQL数据库的,那一台数据库服务器目前经常在晚上秒杀活动的时候,瞬时并发量达到上万。所以最近几天明显发现数据库的负载越来越高,比如CPU、IO、内存、磁盘的负载几乎都快要到极限


2、方案一:堆机器

  • 订单系统部署更多的机器,可以抗下更高的并发。订单系统自己是可以通过部署更多的机器进行线性扩展
  • 数据库呢?是不是也要部署更多的服务器,进行分库分表,然后让更多的数据库服务器来抗超高的数据库高并发访问?

分库分表,就是把目前的一台数据库服务器变成多台数据库服务器,然后把一张订单表变成多张订单表。例子,目前假设订单表里有1200万条数据,然后有一台数据库服务器,如果我们现在变成3台数据库服务器,那么可以在每台数据库服务器里放400万订单数据,这就是所谓的分库分表。

问题:比如未来订单系统的整体访问压力达到了每秒3万请求了,此时订单系统通过扩容可以部署很多机器,然后其中1万请求写入到一台数据库服务器,1万请求写入到另一台数据库服务器,另外1万请求写入最后一台数据库服务器,就好像下面的图这样子。

堆机器的方法来解决这个问题,必然存在一个问题,就是随着你的用户量越来越大,你的并发请求越来越多,会导致你要不 停的增加更多的机器。如果现在你每秒的并发请求量是1万,可能你就需要20台4核8G的订单服务器+1台高配置的数据库服务器,就可以扛下来了。 但是如果你未来用户量增长10倍,每秒有10万并发请求呢?难道你就直接让订单系统部署200台机器?然后将数据库服务器增加到10 台?这样会导致你公司的服务器成本急剧飙升!

3、方案二:架构设计

说明:为了应对秒杀活动这种特殊场景,不能采取无限制的扩容服务器的方案,而应该是利用各种技术去合理设计更加优秀的架构,在有限的机器资源条件下,去抗下更高的并发。

3.1、高并发的商品详情页请求

  • 平时大量的用户是怎么参与到秒杀活动里来的?

首先大量用户会拿着APP不停的刷新一个秒杀商品的页面。

  • 秒杀商品页面是从哪儿加载出来的呢?
    从 商品详情页系统中加载出来的,首先这个商品详情页系统就是在秒杀活动开始之前最先被大量用户高并发访问的一个系统了!

故可能几十万人,甚至百万级的用户,会同一时间频繁的访问同一个秒杀商品的页面

3.2、商品团队的秒杀架构优化:页面数据静态化

如果让秒杀商品页面是动态化的,那么每次一个用户只要访问这个商品详情页,就必须发送一次请求到后端的商品详情页系统来获取数据。 比如商品的标题、副标题、价格、优惠策略、库存、大量的图片、商品详情说明、售后政策等等,这一大堆的东西都是商品详情页的数据。
那么你可以选择让用户浏览这个秒杀商品的时候,每次都发送请求到后台去加载这些数据过来,然后渲染出来给用户看这个商品页面这就是所谓的动态模式。

首先第一步,秒杀商品页面必须是将其数据做到静态化,即 提前就从数据库里把这个页面需要的数据都提取出来组装成一份静态数据放在别的地方,避免每次访问这个页面都要访问后端数据库。

3.3、商品团队的秒杀架构优化:多级缓存

使用CDN + Nginx + Redis的多级缓存架

CDN缓存就是我们多级缓存架构里的第一级缓存:不同地方的用户在加载这个秒杀商品的详情页数据时,都是从就近的CDN上加载的,不需要每次请求都发送到我们公司在上海的机房去。
就是说秒杀商品详情页的数据,首先会放一份在离用户地理位置比较近的CDN上。

CDN:比如我们公司的机房在上海,系统也部署在上海,那么对于陕西的用户,难道每次都要发送请求到我们的上海机房里来获取数据吗? 不是,我们完全可以将一些静态化好的数据放在陕西的一个CDN上。同样对于广州的用户,可以把这些静态化好的数据放在广州的 CDN上,这个CDN现在都是各种云厂商提供的服务。

  • 因为缓存过期之类的问题,CDN上没有用户要加载的商品详情页数据怎么办呢?

在Nginx这样的服务器里做一级缓存:在Nginx中是可以基于Lua脚本实现本地缓存的,我们可以提前把秒杀商品详情页的数据放到Nginx中进行缓存,如果请求发送过来, 可以从Nginx中直接加载缓存数据,不需要把请求转发到我们商品系统上去。 

 

  • 如果在Nginx服务器上也没加载到秒杀商品的数据呢?

此时就可以由Nginx中的Lua脚本发送请求到Redis集群中去加载我们提前放进去的秒杀商品数据,

  • 在Redis中还是没有找到呢?
    那么就由Nginx中的Lua脚本直接把请求转发到商品详情页系统里去加载就可以了,此时就会直接从数据库中加载数据出来。

一套方案:我们就可以把用于秒杀活动的商品详情页数据进行静态化,然后把静态化以后的一串商品数据(比如可能就是一 个大的JSON串)放到CDN、Ngxin、Redis组成的多级缓存里去,这样大量的用户同时访问这个秒杀商品页面就对我们的商品系统本身没什么压力了。

因为分布在全国各地的用户的大量请求都是分散发送给各个地方的CDN的,所以CDN就分摊掉了大量的请求。而即使请求到达了我们的后台系统,都是由轻松单机抗10万+并发的Nginx和Redis来返回商品数据的。

总结:

页面数据静态化+多级缓存

3.4、基于MQ的秒杀订单系统架构

到时间,页面上会让一个立即抢购的按钮变成可以点击的状态,在那之前这个按钮是灰色的,不能点击。然后瞬间可能几十万甚至上百万人会同时点击这个按钮,尝试对后台发起请求去抢购这个商品。

在这个过程中,实际上大量的人要做的事情,就是跟之前正常购买商品一样的事情,比如下订单、支付、扣减库存以及后续一系列事情。

3.4.1、用答题的方法避免作弊抢购以及延缓下单

  • 可能会有人自己写一个抢购的脚本或者作弊软件,疯狂的发送请求去抢商品
    首先就需要在客户端增加一个秒杀答题的功能:一般来说,现在你要参与抢购,都会让你点击按钮之后先进行答题,就是说先弹出来一个框,让你回答一个问题,回答正确了你才能发起抢购的请求。 这个办法是非常有效的,因为首先他避免了一些作弊软件去发送抢购请求,另外就是不同的人答题的速度是不一样的,所以可以通过这 个答题让不同的人发送请求的时间错开,不会在一个时间点发起请求。

3.4.2、为秒杀独立出来一套订单系统

假设你有100万用户在这个时间段很活跃都会来购买商品,但是可能只有其中50万用户在参与秒杀活动,同一时间发送了大量的抢购请求到后台系统,但是同时还有很多其他的用户这个时候并不在参与秒杀系统,他们在进行其他商品的常规性浏览和下单。 因此这个时候如果你让秒杀下单请求和普通下单请求都由一套订单系统来承载,那么可能会导致秒杀下单请求耗尽了订单系统的资源,或者导致系统不稳定,然后导致其他普通下单请求也出现问题,没有办法完成的下单。

一般我们会对订单系统部署两个集群,一个集群是秒杀订单系统集群,一个集群是普通订单系统集群。

3.4.3、基于Redis实现下单时精准扣减库存

秒杀商品一般是有数量的限制的,比如几十万人可能就抢购1万个特价商品。在后台系统中我们首先需要做的一个事情,就是扣减库存。如果还是直接由订单系统调用库存系统的接口,然后访问库存数据库去扣减,那么势必导致瞬时压力过大,可能让库存系统的压力很大。

通常在秒杀场景下,一般会将每个秒杀商品的库存提前写入Redis中,然后当请求到来之后,就直接对Redis中的库存进行扣减 。Redis是可以轻松用单机抗每秒几万高并发的,因此这里就可以抗下高并发的库存扣减。

比如:我们可能总共就1万件秒杀商品,那么其实最多就是前1万个到达的请求可以成功从Redis中扣减库存,抢购到这个商品接着后续的请求在从Redis里扣减库存的时候,都会发现库存已经没了,就无法抢购到商品了。

 3.4.4、抢购完毕之后提前过滤无效请求

其实在Redis中的库存被扣减完之后,就说明后续其他的请求都没有必要发送到秒杀系统中了,因为商品已经被抢购完毕了。此时我们可以让Nginx在接收到后续请求的时候,直接就把后续请求过滤掉。
比如一旦商品抢购完毕,可以在ZooKeeper中写入一个秒杀完毕的标志位,然后ZK会反向通知Nginx中我们自己写的Lua脚本,通过Lua脚本后续在请求过来的时候直接过滤掉,不要向后转发。

如果有50万人同时抢购1万件商品,其实最多就前面1万人发送的请求会抢购到商品,之后的49万请求都会在Nginx层面直接被拦截掉,过滤掉这些无效请求,返回响应告诉他们商品库存已经没了。这样可以最大幅度削减对后端秒杀系统的请求压力。

3.4.5**、瞬时高并发下单请求进入RocketMQ进行削峰**

问题:哪怕是有1万件商品同时被1万人秒杀成功了,那么可能瞬间会有1万请求涌入正常的订单系统进行后续的处理,此时可能还是会有瞬间上万请求访问到订单数据库中创建订单。

  • 对于秒杀系统而言,如果判断发现通过Redis完成了库存扣减,此时库存还大于0,就说明秒杀成功了需要生成订单,此时就直接发送一个消息到RocketMQ中即可。
  • 然后让普通订单系统从RocketMQ中消费秒杀成功的消息进行常规性的流程处理即可,比如创建订单等等。这样的话,瞬间上万并发的压力会被RocketMQ轻松抗下来,然后普通的订单系统可以根据自己的工作负载慢慢的从RocketMQ中拉取秒杀成功的消息,然后进行后续操作就可以了,不会对订单数据库造成过大的压力。
  • 否则如果你让瞬间产生的一万或者几万的订单请求直接访问订单数据库,必然还是会让他压力过大,需要额外增加机器,那是没有必要的。
  • 因此在这里利用RocketMQ抗下每秒几万并发的下单请求,然后让订单系统以每秒几千的速率慢慢处理就可以了,也就是延迟个可能几十秒,这些下单请求就会处理完毕。

 总结

我们架构优化的核心就是独立出来一套系统专门处理,避免高并发请求落在MySQL上** ,**因为MySQL天生不擅长抗高并发,我们需要通过Redis、Nginx、RocketMQ这些天生轻松可以单机抗几万甚至十万并发的系统来优化架构。

相关文章