秒杀系统相信很多人都见过,比如京东或者淘宝的秒杀,小米手机的秒杀,那么后台怎么样秒杀系统的实现?我们如何设计一个秒杀系统?秒杀系统应该考虑哪些问题?如何设计一个健壮的秒杀系统?在这篇文章中,我们将讨论这个问题。秒杀超卖问题需要考虑哪些问题秒杀业务场景分析,最重要的一点就是超卖问题,如果库存只有100,最后却超卖了200,一般来说,秒杀系统是比较低级的,如果超卖会严重影响公司的财产利益,所以首先要做的就是解决超卖商品的问题。高并发秒杀具有时间短、并发量大的特点。秒杀的持续时间只有几分钟。大多数公司为了制造轰动效应,都会以极低的价格吸引用户,所以参与抢购的用户会非常多。短时间内会有大量的请求涌入。如何防止后端并发过高导致缓存崩溃或失效,压垮数据库是需要考虑的问题。接口反刷屏目前大部分秒杀都会针对秒杀对应的软件出来。这种软件会模拟不断向后台服务器发送请求。每秒有数百次是很常见的。发起的请求也需要我们有针对性的考虑。秒杀URL对于普通用户来说,看到的是一个比较简单的秒杀页面。在到达指定时间之前,秒杀按钮是灰色的。到达指定时间后,灰色按钮变为可单击。这部分是为新手用户准备的。如果你是有一点电脑基础的用户,通过F12查看浏览器的网络会看到闪购的url,也可以通过请求特定的软件来实现闪购。或者提前知道秒杀url的人,一请求就可以直接实现秒杀。我们需要考虑解决这个问题。数据库设计峰值有压垮我们服务器的风险。如果用在同一个数据库中,再加上我们的其他业务,很可能会牵连影响到其他业务。如何防止这种问题的发生,即使秒杀出现宕机或者服务器卡死,也要尽量不影响正常的在线业务。根据大量请求“高并发”的考虑,即使使用缓存也不足以应对短期高并发流量的冲击。如何承载如此庞大的流量,同时提供稳定、低时延的服务保障,是需要面对的一大挑战。让我们来计算一下。如果我们使用Redis缓存,单台Redis服务器可以承受的QPS大约是4W。如果一个秒杀吸引到足够多的用户,单个QPS可能会达到几十万。单个Redis服务器仍然不足以支撑如此巨大的请求量。缓存会被击穿,直接渗透到DB中,从而破坏MySQL,后台会报大量错误。秒杀系统设计及技术方案秒杀系统的数据库设计是针对“数据库设计”提出的秒杀数据库问题,所以要单独设计一个秒杀数据库,防止高并发访问拖垮整个网站秒杀活动。这里只需要两张表,一张是秒杀订单表,一张是秒杀商品表。其实应该有几张表,product表:可以关联goods_id来查找具体的商品信息,商品图片,名称,平时价,闪购价等,user表:用户昵称,用户手机号都可以根据用户user_id、收货地址等附加信息查询,本具体示例不再赘述。秒杀URL的设计为了防止有程序接入经验的人通过订单页面的url直接访问后台界面做秒杀产品,我们需要把秒杀URL做成动态的,即使是整个系统开发的人在启动URL之前无法知道秒杀。具体方法是用md5加密一串随机字符作为秒杀的url,然后前端访问后台获取具体的url,后台验证通过后才能继续秒杀。静态秒杀页面将所有的商品描述、参数、交易记录、图片、评价等写入一个静态页面。用户请求不需要访问后端服务器或数据库,直接在前端客户端生成。这样可以尽可能的减轻服务器的压力。具体方法可以使用freemarker模板技术构建网页模板,填写数据,然后渲染网页。单台Redis升级成集群Redis秒杀是读多写少的场景,所以用Redis做缓存是再完美不过了。但是考虑到缓存崩溃的问题,我们应该搭建一个Redis集群,使用sentinel模式来提高Redis的性能和可用性。使用NginxNginx是一个高性能的web服务器,它的并发能力可以达到几万,而Tomcat只有几百。通过Nginx映射客户端请求,然后分发到后台Tomcat服务器集群,可以大大提高并发能力。精简SQL的一个典型场景是在扣库存的时候。传统的方法是先查询库存,再更新。这种情况下需要两条SQL,但实际上我们用一条SQL就可以完成。可以使用这个方法:updatemiaosha_goodssetstock=stock-1wheregoos_id={#goods_id}andversion=#{version}andsock>0;这样可以保证库存不会超卖,一次性更新库存。注意这里使用的是版本号乐观锁,比悲观锁有更好的性能。Redis预先减少了很多对库存的请求,都需要在后台查询库存。这是一个经常阅读的场景。您可以使用Redis来预先减少库存。可以在秒杀启动前设置Redis中的值,如redis.set(goodsId,100)。这里预存库存为100,可以设置为常量。每次下单成功后,Integerstock=(Integer)redis.get(goosId);然后判断sock的值,小于常数值则减1;但是注意取消的时候需要增加库存,增加库存的时候也要注意不要超过设置之间(查询库存和扣除库存需要原子操作,这里可以使用lua脚本时间)。下次下单获取库存时,直接从Redis中查询即可。接口限流秒杀的最终本质是更新数据库,但是无效请求非常多。我们最终需要做的是如何过滤掉这些无效的请求,以防止它们渗透到数据库中。对于限流,有很多方面可以入手:前端限流第一步是通过前端限流,用户点击秒杀按钮后发起请求,然后无法点击进去接下来的5秒(通过将按钮设置为禁用)。这个小计划的开发成本不高,但它奏效了。xx秒内同一用户直接拒绝重复请求的具体秒数视实际业务和秒杀人数而定,一般以10秒为限。具体方法是利用Redis的key过期策略,首先对每一个requestfromStringvalue=redis.get(userId);如果获取到的值为空或null,则表示这是一个有效的请求,然后释放该请求。如果不为空,说明是重复请求,直接丢弃该请求。如果有效,则使用redis.setexpire(userId,value,10)。value可以是任何值。一般最好放业务属性。这是设置userId为key,过期时间为10秒(10秒后,key对应的值自动为null)。令牌桶算法限制接口流量的策略有很多种。这里我们使用令牌桶算法。令牌桶算法的基本思想是每次请求都尝试获取令牌,后端只处理持有令牌的请求。我们可以限制令牌生产的速度和效率。Guava提供了RateLimterAPI供我们使用。这里举一个简单的例子,注意Guava需要引入:publicclassTestRateLimiter{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个token/秒)来不及产生,它肯定不会获取并返回false。这种限流策略的效率如何?如果我们的并发请求是400万个瞬时请求,令牌生成效率设置为每秒20个,每次尝试获取令牌的时间为0.05秒,那么最终的测试结果是每次只会释放4个左右的请求,大量的请求会被拒绝,这是令牌桶算法的优秀之处。异步排序是为了提高排序效率,防止排序服务失败。下单操作需要异步处理。最常用的方法是使用队列。队列最显着的三个优点是:异步、削峰和解耦。这里可以使用RabbitMQ。在后台进行限流和库存校验后,流入这一步的是有效请求。然后发送到队列,队列接受消息并异步下单。下单且入库没有问题后,即可短信通知用户秒杀成功。如果失败,可以使用补偿机制重试。服务降级如果在秒杀过程中服务器宕机或服务不可用,则应做好备份工作。上一篇博客介绍了通过Hystrix进行服务熔断降级,可以开发一个备份服务。如果服务器真的宕机了,会直接给用户友好的提示让其返回,而不是直接冻结、服务器错误等生硬的反馈。总结秒杀流程图:这是我设计的秒杀流程图。当然,不同的加标量针对不同的技术选择。这个过程可以支持几十万的流量。如果是几千万、几十亿,那就得重新设计了。比如数据库的分库分表,队列改用Kafka,Redis增加集群数量等手段。通过这个设计,我们主要是想展示一下我们是如何应对高并发处理的,并着手尝试解决的。工作中多想多做才能提升我们的能力水平,加油!
