前言
秒杀系统京东和淘宝秒杀,小米手机秒杀等,相信很多人都见过。那么秒杀系统的后台怎么实现呢? 怎么设计秒杀系统? 关于秒杀系统应该考虑哪些问题? 怎么设计结实的秒杀系统? 这次让我们讨论一下这个问题:
一:秒杀应该考虑哪些问题
1.1 )超售问题分析秒杀的商业场景最重要的是超卖问题。 假设只有100个的商品最终超卖200个,一般秒杀系统的价格比较低,超卖会严重影响公司的财产利益,因此首先要解决商品超卖问题。
1.2 )高合并
秒杀具有时间短、并发量大的特点,秒杀持续时间只有几分钟,但一般公司为了产生轰动效应,以非常低的价格吸引用户,因此参与抢购的用户非常多。
短时间内大量请求蜂拥而至,后端如何避免因同时频发而导致缓存被破坏或失效,是破坏数据库需要考虑的问题。
1.3 )接口的刷子对策
现在的秒杀大多会出现支持秒杀的软件。 这样的软件模拟了不断向后台服务器提出请求。 每秒数百次是常见的。 如何防止这种软件的重复无效要求,防止不断提出要求,也需要我们明确考虑
1.4 )秒杀url
对于幸福的蓝天来说,看的只是比较简单的秒杀页面,在没有达到规定时间的情况下,秒杀按钮呈灰色,到达规定时间后,灰色按钮变为可点击状态。 本部分适用于sldmd用户
如果是稍微熟悉电脑的用户,可以在F12上通过浏览器的网络浏览秒杀的url,通过特定软件的请求也可以实现秒杀。
或者事先知道秒杀url的人,请求后很快就会实现秒杀。 这个问题需要我们考虑解决。
1.5 :数据库设计
秒杀有打垮我们服务器的风险,如果让它和我们其他业务用在同一个数据库里,结合在一起,很可能会涉及和影响其他业务。
为了避免这些问题,您必须确保秒杀或服务器宕机不会影响在线正常工作。
1.6 :大量请求提问
1.2的想法是,使用缓存不足以应对短时间的高并发通信量的冲击。 如何支持如此庞大的访问量,同时提供稳定的低延迟服务保证,是面临的一大课题。
让我算一下账本。 假设使用redis缓存,一台redis服务器所能承受的QPS大约为4W左右。 如果一秒钟能吸引的用户数量足够多,一个QPS就有可能达到数十万人。 单个redis不足以支撑如此巨大的请求数量。 缓存被破坏,直接渗透到数据库中,mysql被破坏。 后台会报告大量错误。
二:秒杀系统的设计和技术方案
2.1 :秒杀系统数据库设计关于1.5中提出的秒杀数据库问题,应该单独设计秒杀数据库,防止秒杀活动高并发访问导致整个网站崩溃。
这里需要的只有两个表,一个是秒杀订单,一个是秒杀商品表
其实,应该还有几张表。 商品表:可以关联goods_id来调查具体的商品信息。 商品图像、名称、平时价格、秒杀价格等。 另外,用户表:可以根据用户user_id查看用户的昵称、用户的手机号码、收件人等附加信息。 这不具体举例。
2.2 :秒杀url的设计
为了避免有访问程序经验的人直接从订单页面的url访问后台界面秒杀商品,需要将秒杀的url动态化,即使是开发整个系统的人在秒杀开始之前也不会知道秒杀的url
具体可以用md5加密一系列随机字符作为秒杀的url,用前端访问后台获取具体的url,后台校验通过后继续秒杀。
2.3 )秒杀页面的静态化
将商品的说明、参数、成交记录、图像、评估等全部写入一个静态页面,用户的请求是直接在前台客户端生成,无需访问后端服务器或经由数据库
具体方法是使用freemarker模板技术,创建网页模板,输入数据,渲染网页。
2.4 )从单体redis升级到集群redis
秒杀是一个很多阅读和写作的场景,非常适合用redis制作缓存。 但是,考虑到缓存破坏的问题,应该通过构建redis集群和采用哨兵模式来提高redis的性能和可用性。
>2.5:使用nginxnginx是一个高性能web服务器,它的并发能力可以达到几万,而tomcat只有几百。通过nginx映射客户端请求,再分发到后台tomcat服务器集群中可以大大提升并发能力。
2.6:精简sql
典型的一个场景是在进行扣减库存的时候,传统的做法是先查询库存,再去update。这样的话需要两个sql,而实际上一个sql我们就可以完成的。
可以用这样的做法:
update miaosha_goods set stock =stock-1 where goos_id ={#goods_id} and version = #{version} and sock>0;
这样的话,就可以保证库存不会超卖并且一次更新库存,还有注意一点这里使用了版本号的乐观锁,相比较悲观锁,它的性能较好。
2.7:redis预减库存
很多请求进来,都需要后台查询库存,这是一个频繁读的场景。可以使用redis来预减库存,在秒杀开始前可以在redis设置
比如 redis.set(goodsId,100),这里预放的库存为100可以设值为常量),每次下单成功之后,Integer stock = (Integer)redis.get(goosId); 然后判断sock的值,如果小于常量值就减去1。
不过注意当取消的时候,需要增加库存,增加库存的时候也得注意不能大于之间设定的总库存数(查询库存和扣减库存需要原子操作,此时可以借助lua脚本)下次下单再获取库存的时候,直接从redis里面查就可以了。
2.8:接口限流
秒杀最终的本质是数据库的更新,但是有很多大量无效的请求,我们最终要做的就是如何把这些无效的请求过滤掉,防止渗透到数据库。
限流的话,需要入手的方面很多:
2.8.1:前端限流
首先第一步就是通过前端限流,用户在秒杀按钮点击以后发起请求,那么在接下来的5秒是无法点击(通过设置按钮为disable)。这一小举措开发起来成本很小,但是很有效。
2.8.2:同一个用户xx秒内重复请求直接拒绝
具体多少秒需要根据实际业务和秒杀的人数而定,一般限定为10秒。
具体的做法就是通过redis的键过期策略,首先对每个请求都从String value = redis.get(userId);
如果获取到这个value为空或者为null,表示它是有效的请求,然后放行这个请求。如果不为空表示它是重复性请求,直接丢掉这个请求。
如果有效,采用redis.setexpire(userId,value,10).value可以是任意值,一般放业务属性比较好,这个是设置以userId为key,10秒的过期时间(10秒后,key对应的值自动为null)
2.8.3:令牌桶算法限流
接口限流的策略有很多,我们这里采用令牌桶算法。
令牌桶算法的基本思路是每个请求尝试获取一个令牌,后端只处理持有令牌的请求,生产令牌的速度和效率我们都可以自己限定,guava提供了RateLimter的api供我们使用。
以下做一个简单的例子,注意需要引入guava
上面代码的思路就是通过RateLimiter来限定我们的令牌桶每秒产生1个令牌(生产的效率比较低),循环10次去执行任务。
acquire会阻塞当前线程直到获取到令牌,也就是如果任务没有获取到令牌,会一直等待。那么请求就会卡在我们限定的时间内才可以继续往下走,这个方法返回的是线程具体等待的时间。
执行如下:
可以看到任务执行的过程中,第1个是无需等待的,因为已经在开始的第1秒生产出了令牌。
接下来的任务请求就必须等到令牌桶产生了令牌才可以继续往下执行。如果没有获取到就会阻塞(有一个停顿的过程)。
不过这个方式不太好,因为用户如果在客户端请求,如果较多的话,直接后台在生产token就会卡顿(用户体验较差),它是不会抛弃任务的,我们需要一个更优秀的策略:如果超过某个时间没有获取到,直接拒绝该任务。
接下来再来个案例:
其中用到了tryAcquire方法,这个方法的主要作用是设定一个超时的时间,如果在指定的时间内预估(注意是预估并不会真实的等待),如果能拿到令牌就返回true,如果拿不到就返回false。
然后我们让无效的直接跳过,这里设定每秒生产1个令牌,让每个任务尝试在0.5秒获取令牌,如果获取不到,就直接跳过这个任务(放在秒杀环境里就是直接抛弃这个请求);
程序实际运行如下:
只有第1个获取到了令牌,顺利执行了,下面的基本都直接抛弃了,因为0.5秒内,令牌桶(1秒1个)来不及生产就肯定获取不到返回false了。
这个限流策略的效率有多高呢?假如我们的并发请求是400万瞬间的请求,将令牌产生的效率设为每秒20个,每次尝试获取令牌的时间是0.05秒,那么最终测试下来的结果是,每次只会放行4个左右的请求,大量的请求会被拒绝,这就是令牌桶算法的优秀之处。
2.9:异步下单
为了提升下单的效率,并且防止下单服务的失败。需要将下单这一操作进行异步处理。
最常采用的办法是使用队列,队列最显著的三个优点:异步、削峰、解耦。
这里可以采用rabbitmq,在后台经过了限流、库存校验之后,流入到这一步骤的就是有效请求。然后发送到队列里,队列接受消息,异步下单。
下完单,入库没有问题可以用短信通知用户秒杀成功。假如失败的话,可以采用补偿机制,重试。
2.10:服务降级
假如在秒杀过程中出现了某个服务器宕机,或者服务不可用,应该做好后备工作。之前的博客里有介绍通过Hystrix进行服务熔断和降级,可以开发一个备用服务。
假如服务器真的宕机了,直接给用户一个友好的提示返回,而不是直接卡死,服务器错误等生硬的反馈。
三:总结
秒杀流程图:
这就是我设计出来的秒杀流程图,当然不同的秒杀体量针对的技术选型都不一样,这个流程可以支撑起几十万的流量,如果是成千万破亿那就得重新设计了。比如数据库的分库分表、队列改成用kafka、redis增加集群数量等手段。
通过本次设计主要是要表明的是我们如何应对高并发的处理,并开始尝试解决它,在工作中多思考、多动手能提升我们的能力水平,加油!