秒杀抢购系统优化思路详解

前几年,火车票购票网站12306,每到放假高峰期,在线票刷不出来,购买困难,甚至出现了各类插件支持抢票,这样的场景,对于每一个买过票的人,应该印象深入。小米手机的抢购活动,一样异常火爆,在几分钟的时间内,卖出几十万部手机。当一个Web系统,在一秒钟内收到数以万计甚至更多请求时,系统的优化和稳定相当重要。
在面试中,面试官提出这样的问题,应该从哪些角度分析。在工做中,也许没有这么庞大复杂的应用场景,可是,针对网站的优化思路是一致的。本文从技术的角度,分析下应如何设计优化系统,才能保障如此大规模的并发访问。
 
秒杀系统主要解决三大问题:
1、瞬时的高并发访问。抢购和普通的电商销售有所不一样,普通的电商销售,流量是比较平均的,虽然有波峰波谷,但不会特别突出。而抢购是在特定时间点进行的推销活动,抢购开始前,用户不断刷新页面,以得到购买按钮;抢购开始的一瞬间,集中并发购买。
2、数据正确性。抢购毕竟是一种购买行为,须要购买、扣减库存、支付等复杂的流程,在此过程当中,要保证数据的正确性,防止超卖(卖出量超过库存)的发生。
3、防做弊。不管是火车票的购买,仍是低价商品的促销,确定不但愿某些客户买到全部的商品,应尽可能保证公平性。经过购票插件购买火车票,阿里巴巴抢月饼事件等,须要限制技术性用户绕过网站的限制,经过技术手段得到不良收益。
 
解决上述问题,主要有以下的三个思路:访问拦截,分流,限流。
主流的Web站点采用分层的架构设计,若是你的应用尚未采用分层的架构,那么先作分层设计吧。通常来讲,浏览器采用了html/js/css技术,负责数据的展现;反向代理通常采用nginx,负责负载均衡;Web层是指Php,Tomcat等应用服务器,负责用户状态的维护,http协议处理等;service层通常是rpc调用,固然也有用http的,例如spring cloud;数据库存储通常是mongodb,mysql等持久化数据方案。用户的一次数据访问,例如查询商品库存,数据是从上层依次调用到DB,逐层返回数据。
 
所谓访问拦截,是指尽可能把访问拦截在上层,减轻下一层的压力,即离用户访问更近的那一层。下面将从每一层讲解如何作访问拦截。
浏览器访问拦截:产品层面,当用户点击查询或购买按钮后,按钮置灰,防止用户重复提交数据。js层面,限制用户在限定时间内的接口调用次数,或者返回相同的值。例如,用户重复刷新,每秒访问10次接口,变成5秒钟访问一次,并发量将会下降50倍。此种方法,能够拦截90%的小白用户的访问,可是技术型的用户能够绕过js,经过脚本或其余自动化方式调用接口,当年出现的刷票神器,就属于这类范畴。用户量虽小,可是访问量很大。关于防做弊的问题,后续讨论。
CDN加速:CDN的全称是Content Delivery Network,即内容分发网络。其基本思路是尽量避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。简单的来讲,就是把原服务器上数据复制到其余服务器上,用户访问时,那台服务器近访问到的就是那台服务器上的数据。CDN的劣势是内容的变动生效慢,因此仅适用于“几乎不变”的资源,例如引用的js包,图片等。
动静分离与反向代理层访问拦截:动态页面是指根据实时数据渲染的,须要组织数据、渲染页面;静态页面是存储在文件系统的文件,不会根据数据变化而变化,读取速度很快。为了提高效率,应尽量的静态化,用静态页面,替换动态页面。例如,商品信息页,商品信息在发布后,是不会变化的,若是采用动态的方式,访问数据库读取数据,service组装数据,web渲染数据;若是发布商品信息时,就保存下商品信息的静态页面,访问时只须要读取一个文件就够了。
作了动静分离,静态文件的访问应在哪一层返回?不管是tomcat,仍是apache,都支持静态文件的访问,不少时候咱们也是这么作的,把静态文件做为web项目的一部分进行发布。Nginx也支持静态文件的访问,更高效的作法是,把静态文件交由nginx管理,访问nginx直接返回静态数据,减轻Web服务的压力。
Web层和Service层访问拦截:经过上述的访问拦截,进入到web层的,都是动态数据访问。这部分的访问拦截,主要采用缓存的策略,减小对下一层的数据访问。缓存又可分为本地缓存和redis、memcache等缓存中间件。关于缓存,重点关注缓存的淘汰策略。通常有三种方式:超时更新,定时更新,通知更新。
访问拦截,除了减小向下一层的访问,还大幅提升系统的支持用户数。访问拦截,大大减小了每次请求的处理时间,假设:每一个请求原来须要200ms时间,10W的并发量,每秒钟可处理50W的请求;经过访问拦截,每一个请求的处理时间降低到100ms,一样的并发量,每秒钟可处理100W的请求。
经过上述的分析,各层经过访问拦截,系统架构演变成以下的结构。
 
在并发量巨大的场景下,经过上述的优化远远不够的,由于单台服务器的处理能力是有限的,即使在当前硬件设备愈来愈便宜,也不可能无限扩容。分流就是指经过多台服务器,并发的处理请求,减轻单台服务的负载。
DNS轮询:Nginx的处理能力是有限的,单台服务器支持10W左右的并发访问,没有问题。若是更大的负载怎么办?Nginx是应用服务的入口,不能再应用服务这个层次增长服务器,提升并发处理能力。
经过浏览器输入域名访问某个服务,其过程如图所示。DNS轮询是ISP提供的一个服务,不一样的用户访问同一个域名,获取到不一样的IP地址。例如:给www.example.com配置4个IP地址,若是有40W的并发访问,每一个IP将会得到10W的并发访问。固然,域名的IP地址配置,能够支持不一样的策略,例如按照电信运营商分配,按照地域分配等。
Nginx负载均衡:Nginx能够支持10W的并发访问,而应用服务器却达不到这个水准,tomcat通常支持1W的并发访问就很好了。Nginx支持配置请求的代理策略,把请求路由到多个Web服务器处理。Nginx支持的负载均衡策略包括:轮询,权重,ip_hash,fair,url_hash等。
分布式架构的负载策略:Web层调用service,以及service之间的调用,每一个service都须要部署多份。目前最经常使用的两个框架技术,spring cloud和dubbo,都采用客户端负载均衡策略,路由到service的不一样实例。
Redis负载:redis是内存的缓存结构,很是高效,瓶颈在于网络IO,支持几十万的QPS。redis分流,可考虑分片的设计,把数据分配到多台服务器上,减轻每台机器的负载。通常状况下,分片策略多用户redis数据扩容方案。
Mysql读写分离:对写请求,不适合作分流,由于分流后的数据同步是大问题,致使数据不一致。对于写请求,通常采用读写分离的策略,而且能够多台读库。读库应用MyIsam引擎,单独设置合适的索引,提升读性能。从库并非越多越好,由于从库越多,数据延迟越严重,要保持好平衡。
经过上述的分析,各层经过分流策略,系统架构演变成以下的结构。
 
 
访问拦截和分流的策略,主要做用仍是解决并发读的问题。购买、支付等这类“写请求”,不能像读缓存同样,写缓存提升效率,数据持久化成功,才算交易成功。尤为抢购这种模式下,商品数量少,若是多台服务同事写数据,将形成mysql严重的行锁冲突,执行效率远远不如顺序执行。而且大量的所等待,延长单个操做的时长,占用工做线程,产生服务雪崩现象,短期内不能对外提供服务。解决此问题的思路是限流,限制写操做的流量,使其正常运行,不影响业务。
计数器:假设总共100个商品库存,供你们抢货,并发访问极大。能够在Web层作一个计数器,抢单一次计数器加1,计数器到达100后,直接返回抢购失败。一样的道理,计数器亦可在service层实现。这种状况下,假设有10台web服务器,也只会放行100 * 10 = 1000次抢购。
按商品路由:在Web层,把对同一品类商品的抢购路由到一台service处理。在service内,自定义mysql链接池,使对同一个商品的操做,使用同一个链接。这样就实现了对同一商品的顺序处理,避免了锁竞争。
异步化:是指把购买请求的接受和处理异步化。购买请求先放到队列中,这个过程很是高效,返回客户信息。抢购服务订阅消息队列,异步处理购买请求,处理成功给用户发消息。异步化主要解决成产和消费的速度不匹配问题,由此类场景均可以采用。
 
 
对于防做弊问题,是比较容易处理的。由于全部的购买,都是登录用户的行为,能够很方便的根据用户ID进行过滤,只容许一个客户购买一次。在分布式环境下,要解决如何记录用户ID的问题,由于同一个用户可能被不一样的web,不一样的service处理。
全局Cache:在redis中开辟一个空间,记录全部用户的商品购买,处理用户购买请求是,校验缓存中是否已记录此商品的购买,若是已经购买,则不容许。要解决重复提交的问题,可考虑分布式锁。
用户ID路由:参考上一节的按商品路由,咱们一样能够把对一个用户的处理,路由到同一个Service处理,只须要作本地缓存就够了。此种方案最大的问题是,若是服务挂了,数据就错乱了。
相关文章
相关标签/搜索