当前位置: 首页 > 科技观察

详解:如何设计一个健壮的秒杀系统?

时间:2023-03-21 23:06:14 科技观察

前言:秒杀系统相信很多人都见过,比如京东或者淘宝的秒杀,小米手机的秒杀。那么秒杀系统的后台是如何实现的呢?我们如何设计一个秒杀系统?秒杀系统应该考虑哪些问题?如何设计一个健壮的秒杀系统?本期我们将讨论这个问题:目录1:秒杀系统应该考虑的问题2:秒杀系统的设计与技术方案3:系统架构图4:总结1:秒杀应该考虑的问题只有100库存单位,但最终超卖了200个。一般来说,秒杀系统的价格是比较低的。如果超卖,将严重影响公司的财产利益,因此首当其冲的就是解决超卖产品的问题。1.2:高并发秒杀具有时间短,并发量大的特点。秒杀的时长只有几分钟,而且大多数公司为了制造轰动效应,都会以极低的价格吸引用户,所以参与抢购的用户会非常多。短时间内会有大量的请求涌入。如何防止后端并发过高导致缓存崩溃或失效,压垮数据库是需要考虑的问题。1.3:接口反抓取目前大部分秒杀都会针对秒杀对应的软件出来。这种软件会模拟不断向后台服务器发送请求。每秒数百次很常见。如何防止这类软件的重复无效请求,防止连续请求也是我们需要考虑的1.4:对于普通用户来说,秒杀网址只是一个比较简单的秒杀页面。在到达指定时间之前,秒杀按钮是灰色的。到达指定时间后,灰色按钮变为可点击。这部分是为新手用户准备的。如果你是有点电脑的用户,通过F12查看浏览器的网络,会看到秒杀的url,也可以通过特定的软件请求实现秒杀。或者提前知道秒杀url的人,一请求就可以直接实现秒杀。我们需要考虑解决这个问题。1.5:数据库设计秒杀有压垮我们服务器的风险。如果用在同一个数据库中,再加上我们的其他业务,很可能会牵连影响到其他业务。如何防止此类问题的发生,即使秒杀出现宕机或者服务器卡死,也应该尽量不影响正常的在线业务。1.6:大量请求根据1.2的考虑,即使使用缓存仍然不足以应对短期高并发流量的冲击。如何承载如此庞大的流量,同时提供稳定、低时延的服务保障,是需要面对的一大挑战。让我们来计算一下。如果我们使用redis缓存,单台redis服务器可以承受的QPS大约是4W。如果一个秒杀吸引到足够多的用户,单个QPS可能会达到几十万。单个redis服务器仍然不足以支撑如此巨大的请求量。缓存会被分解,直接渗透到DB中,从而打败mysql。后台会报大量错误。2:秒杀系统设计及技术方案2.1:秒杀系统数据库设计针对1.5中提出的秒杀数据库问题,单独设计一个秒杀数据库,防止秒杀活动高并发访问拖垮整个网站.这里只需要两张表,一张是秒杀订单表,一张是秒杀商品表。其实应该有几张表。product表:可以关联goods_id查找具体的商品信息,商品图片,名称,平时价格,秒杀价格等,以及user表:根据user_id可以查询用户的昵称,用户的手机号码、送货地址和其他附加信息。这个具体的例子就不举例了。2.2:秒杀url的设计为了防止有程序访问经验的人通过订单页面的url直接访问后台界面秒杀商品,我们需要将秒杀url动态化,即使是整个开发人员系统无法启动秒杀知道秒杀的url。具体方法是用md5加密一串随机字符作为秒杀的url,然后前端访问后台获取具体的url,后台校验通过后就可以继续秒杀了。2.3:静态秒杀页面将所有商品描述、参数、交易记录、图片、评价等写入一个静态页面。用户请求不需要访问后端服务器,不需要经过数据库,直接在前端客户端生成。这样可以尽可能的减轻服务器的压力。具体方法可以使用freemarker模板技术构建网页模板,填写数据,然后渲染网页。2.4:单台redis升级成集群redis秒杀是读多写少的场景,所以用redis做缓存就完美了。但是考虑到缓存崩溃的问题,我们应该搭建一个redis集群,使用sentinel模式来提高redis的性能和可用性。2.5:使用nginxNginx是一个高性能的web服务器,它的并发能力可以达到几万,而tomcat只有几百。通过nginx映射客户端请求,然后分发到后台tomcat服务器集群,可以大大提高并发能力。2.6:精简SQL的一个典型场景是在扣库存的时候。传统的方法是先查询库存,再更新。在这种情况下,需要两条sql,但实际上我们用一条sql就可以完成。你可以使用这个方法:updatemiaosha_goodssetstock=stock-1wheregoos_id={#goods_id}andversion=#{version}andsock>0;这样可以保证库存不会超卖,一次性更新库存。注意这里使用的版本乐观锁的数量比悲观锁有更好的性能。2.7:很多请求进来redis预减库存,都需要在后台查询库存。这是一个经常阅读的场景。可以使用redis预减库存。可以在秒杀启动前设置redis中的值,如redis.set(goodsId,100),其中预存库存为100,可以设置为常量)。=(整数)redis.get(goosId);然后判断sock的值,如果小于常数值就减1。但是注意取消的时候需要增加库存。增加库存的时候也要注意不要超过设置的总库存之间(查询库存和扣减库存需要原子操作,此时可以使用lua脚本)下次下单的时候拿到库存同样,您可以直接从redis检查它。2.8:接口限流秒杀的终极本质是更新数据库,但是无效请求较多。我们最终需要做的是如何过滤掉这些无效的请求,以防止它们渗透到数据库中。对于限流,有很多方面可以入手:2.8.1:前端限流第一步是通过前端限流。用户点击秒杀按钮后发起请求后,5秒内无法点击(通过设置按钮为disable)。这个小计划的开发成本不高,但它奏效了。2.8.2:xx秒内同一用户直接拒绝重复请求的具体秒数视实际业务和秒杀人数而定,一般以10秒为限。具体方法是利用redis的key过期策略,首先对每一个requestfromStringvalue=redis.get(userId);如果获取到的值为空或null,则表示这是一个有效的请求,然后释放该请求。如果不为空,说明是重复请求,直接丢弃该请求。如果有效,则使用redis.setexpire(userId,value,10)。value可以是任何值。一般最好放业务属性。这是设置userId为key,过期时间为10秒(10秒后,key对应的值自动为null)2.8.3:令牌桶算法限制接口流量的策略有很多,我们使用token桶算法在这里。令牌桶算法的基本思想是每次请求都尝试获取令牌,后端只处理持有令牌的请求。我们可以限制令牌生产的速度和效率。Guava提供了RateLimterAPI供我们使用。.下面做一个简单的例子,注意需要引入guavapublicclassTestRateLimiter{publicstaticvoidmain(String[]args){//1秒生成1个tokenfinalRateLimiterrateLimiter=RateLimiter.create(1);for(inti=0;i<10;i++){//该方法会阻塞线程,直到可以从令牌桶中取出令牌后,再继续向下执行。doublewaitTime=rateLimiter.acquire();System.out.println("任务执行"+i+"等待时间"+waitTime);}System.out.println("执行结束");}}上面的思路代码是通过RateLimiter来限制我们的令牌桶每秒产生1个令牌(生产效率比较低),循环10次执行任务。acquire会阻塞当前线程,直到获取到token,也就是说,如果任务没有获取到token,就会一直等待。那么这个请求就会卡在我们限定的时间内,才可以继续往下走。该方法返回线程的具体等待时间。执行过程如下:可以看到任务执行过程中,不需要等待第一个任务,因为在启动的第一秒就已经产生了token。下一个任务请求必须等到令牌桶产生令牌后才能继续执行。如果没有获取到,就会阻塞(有暂停过程)。但是这个方法不是很好,因为如果用户在客户端请求多了,productiontoken会直接卡在后台(用户体验差),也不会放弃任务。我们需要一个更好的。策略:如果一定时间后没有拿到任务,直接拒绝任务。接下来是另一种情况:publicclassTestRateLimiter2{publicstaticvoidmain(String[]args){finalRateLimiterrateLimiter=RateLimiter.create(1);for(inti=0;i<10;i++){longtimeOut=(long)0.5;booleanisValid=rateLimiter.tryAcquire(timeOut,TimeUnit.SECONDS);System.out.println("Task"+i+"是否执行有效:"+isValid);if(!isValid){continue;}System.out.println("Task"+i+"In");}System.out.println("End");}}的实现,使用了tryAcquire方法。该方法的主要功能是设置一个超时时间。如果估计在指定时间内(注意是估计,估计不会真的等),如果能拿到token就返回true,拿不到就返回false。然后我们直接跳过无效的。这里我们设置每秒产生1个token,让每个task在0.5秒内尝试拿到token。如果获取不到,直接跳过这个任务(在秒杀环境下是直接丢弃这个请求);程序实际运行是这样的:只有第一个获取到token并顺利执行,后面的基本直接丢弃,因为0.5秒内,令牌桶(每秒1个)会确定是不是太latetoproduce如果获取不到,返回false。这种限流策略的效率如何?假设我们的并发请求是400万个瞬时请求,token生成效率设置为每秒20个,每次尝试获取token的时间为0.05秒,那么最终测试的结果是只有4个左右每次请求都会被释放,大量的请求会被拒绝。这就是令牌桶算法的优秀之处。2.9:异步下单是为了提高下单效率,防止下单服务失败。下单操作需要异步处理。最常用的方法是使用队列。队列最显着的三个优点是:异步、削峰和解耦。这里可以使用Rabbitmq。在后台进行限流和库存校验后,有效的请求就流入这一步。然后发送到队列,队列接受消息并异步下单。下单且入库没有问题后,即可短信通知用户秒杀成功。如果失败,可以使用补偿机制重试。2.10:服务降级如果在秒杀过程中出现服务器宕机或者服务不可用,需要做好备份工作。在之前的博客中介绍过通过Hystrix进行服务熔断降级可以开发一个备份服务。如果服务器真的宕机了,直接给用户友好提示返回,而不是直接卡顿、服务器错误等生硬的反馈。三:总结闪购流程图:这是我设计的闪购流程图。当然,不同闪存卷的技术选择是不同的。这个过程可以支持几十万的流量。如果是几千万或者几十亿那么就得重新设计了。比如数据库的分库分表,队列改为使用kafka,redis增加集群数量等方式。通过这个设计,我们主要是想展示一下我们是如何应对高并发处理的,并着手尝试解决的。在工作中多想多做,才能提高我们的能力水平。快点!如果本博客有任何错误,请指出,将不胜感激。