说到高并发,其实咱们中国互联网人最有发言权。中国人口最多,并且特有的社会结构刚好是互联网精神发挥的沃土,因此互联网行业在中国的迅猛发展也属情理之中。而中国现实社会中的地域发展不平衡、产业结构过分区域化、交通不够便利及小企业信息不顺畅等一道道壁垒,反而成就了电商的独特的商业空间。css
伴随着电商的蓬勃发展,抢购和秒杀活动做为营销的重要手段,也对电商系统提出了愈来愈高的要求。当双十一已经从一个简单的噱头演变为可让全民熬夜万众秒杀的头条事件时,电商系统所面对的高并发压力毫不亚于春节的微信红包系统。html
今天,咱们就从实战的角度,对电商的抢购秒杀系统作一次深刻完全的解析:前端
正常电子商务流程程序员
(1)查询商品;(2)建立订单;(3)扣减库存;(4)更新订单;(5)付款;(6)卖家发货web
秒杀业务的特性redis
(1)低廉价格;(2)大幅推广;(3)瞬时售空;(4)通常是定时上架;(5)时间短、瞬时并发量高;算法
假设某网站秒杀活动只推出一件商品,预计会吸引1万人参加活动,也就说最大并发请求数是10000,秒杀系统须要面对的技术挑战有:数据库
对现有网站业务形成冲击json
秒杀活动只是网站营销的一个附加活动,这个活动具备时间短,并发访问量大的特色,若是和网站原有应用部署在一块儿,必然会对现有业务形成冲击,稍有不慎可能致使整个网站瘫痪。后端
解决方案:将秒杀系统独立部署,甚至使用独立域名,使其与网站彻底隔离
。
高并发下的应用、数据库负载
用户在秒杀开始前,经过不停刷新浏览器页面以保证不会错过秒杀,这些请求若是按照通常的网站应用架构,访问应用服务器、链接数据库,会对应用服务器和数据库服务器形成负载压力。
解决方案:从新设计秒杀商品页面,不使用网站原来的商品详细页面,页面内容静态化,用户请求不须要通过应用服务
。
忽然增长的网络及服务器带宽
假设商品页面大小200K(主要是商品图片大小),那么须要的网络和服务器带宽是2G(200K×10000),这些网络带宽是由于秒杀活动新增的,超过网站平时使用的带宽。
解决方案:由于秒杀新增的网络带宽,必须和运营商从新购买或者租借。为了减轻网站服务器的压力,须要将秒杀商品页面缓存在CDN,一样须要和CDN服务商临时租借新增的出口带宽
。
直接下单
秒杀的游戏规则是到了秒杀才能开始对商品下单购买,在此时间点以前,只能浏览商品信息,不能下单。而下单页面也是一个普通的URL,若是获得这个URL,不用等到秒杀开始就能够下单了。
解决方案:为了不用户直接访问下单页面URL,须要将改URL动态化,即便秒杀系统的开发者也没法在秒杀开始前访问下单页面的URL。办法是在下单页面URL加入由服务器端生成的随机数做为参数,在秒杀开始的时候才能获得
。
如何控制秒杀商品页面购买按钮的点亮
购买按钮只有在秒杀开始的时候才能点亮,在此以前是灰色的。若是该页面是动态生成的,固然能够在服务器端构造响应页面输出,控制该按钮是灰色还 是点亮,可是为了减轻服务器端负载压力,更好地利用CDN、反向代理等性能优化手段,该页面被设计为静态页面,缓存在CDN、反向代理服务器上,甚至用户浏览器上。秒杀开始时,用户刷新页面,请求根本不会到达应用服务器。
解决方案:使用JavaScript脚本控制,在秒杀商品静态页面中加入一个JavaScript文件引用
,该JavaScript文件中包含 秒杀开始标志为否;当秒杀开始的时候生成一个新的JavaScript文件(文件名保持不变,只是内容不同),更新秒杀开始标志为是,加入下单页面的URL及随机数参数(这个随机数只会产生一个,即全部人看到的URL都是同一个,服务器端能够用redis这种分布式缓存服务器来保存随机数)
,并被用户浏览器加载,控制秒杀商品页面的展现。这个JavaScript文件的加载能够加上随机版本号(例如xx.js?v=32353823),这样就不会被浏览器、CDN和反向代理服务器缓存
。
这个JavaScript文件很是小,即便每次浏览器刷新都访问JavaScript文件服务器也不会对服务器集群和网络带宽形成太大压力。
如何只容许第一个提交的订单被发送到订单子系统
因为最终可以成功秒杀到商品的用户只有一个,所以须要在用户提交订单时,检查是否已经有订单提交。若是已经有订单提交成功,则须要更新 JavaScript文件,更新秒杀开始标志为否,购买按钮变灰。事实上,因为最终可以成功提交订单的用户只有一个,为了减轻下单页面服务器的负载压力, 能够控制进入下单页面的入口,只有少数用户能进入下单页面,其余用户直接进入秒杀结束页面。
解决方案:假设下单服务器集群有10台服务器,每台服务器只接受最多10个下单请求。在尚未人提交订单成功以前,若是一台服务器已经有十单了,而有的一单都没处理,可能出现的用户体验不佳的场景是用户第一次点击购买按钮进入已结束页面,再刷新一下页面,有可能被一单都没有处理的服务器处理,进入了填写订单的页面,能够考虑经过cookie的方式来应对,符合一致性原则
。固然能够采用最少链接的负载均衡算法
,出现上述状况的几率大大下降。
如何进行下单前置检查
若是超过10条,直接返回已结束页面给用户;
若是未超过10条,则用户可进入填写订单及确认页面;
已超过秒杀商品总数,返回已结束页面给用户;
未超过秒杀商品总数,提交到子订单系统;
检查全局已提交订单数目:
下单服务器检查本机已处理的下单请求数目:
秒杀通常是定时上架
该功能实现方式不少。不过目前比较好的方式是:提早设定好商品的上架时间,用户能够在前台看到该商品,可是没法点击“当即购买”的按钮。可是须要考虑的是,有人能够绕过前端的限制,直接经过URL的方式发起购买
,这就须要在前台商品页面,以及bug页面到后端的数据库,都要进行时钟同步。越在后端控制,安全性越高。
定时秒杀的话,就要避免卖家在秒杀前对商品作编辑带来的不可预期的影响。这种特殊的变动须要多方面评估。通常禁止编辑,如需变动,能够走数据订正多的流程。
减库存的操做
有两种选择,一种是拍下减库存
另一种是付款减库存
;目前采用的“拍下减库存”
的方式,拍下就是一瞬间的事,对用户体验会好些。
库存会带来“超卖”的问题:售出数量多于库存数量
因为库存并发更新的问题,致使在实际库存已经不足的状况下,库存依然在减,致使卖家的商品卖得件数超过秒杀的预期。
方案:采用乐观锁
update auction_auctions setquantity = #inQuantity#where auction_id = #itemId# and quantity = #dbQuantity#
秒杀器的应对
秒杀器通常下单个购买及其迅速,根据购买记录能够甄别出一部分。能够经过校验码达到必定的方法,这就要求校验码足够安全,不被破解,采用的方式有:秒杀专用验证码,电视公布验证码,秒杀答题
。
尽可能将请求拦截在系统上游
传统秒杀系统之因此挂,请求都压倒了后端数据层,数据读写锁冲突严重,并发高响应慢,几乎全部请求都超时,流量虽大,下单成功的有效流量甚小【一趟火车其实只有2000张票,200w我的来买,基本没有人能买成功,请求有效率为0】。
读多写少的经常使用多使用缓存
这是一个典型的读多写少
的应用场景【一趟火车其实只有2000张票,200w我的来买,最多2000我的下单成功,其余人都是查询库存,写比例只有0.1%,读比例占99.9%】,很是适合使用缓存
。
秒杀系统为秒杀而设计,不一样于通常的网购行为,参与秒杀活动的用户更关心的是如何能快速刷新商品页面,在秒杀开始的时候抢先进入下单页面,而不是商品详情等用户体验细节,所以秒杀系统的页面设计应尽量简单。
商品页面中的购买按钮只有在秒杀活动开始的时候才变亮,在此以前及秒杀商品卖出后,该按钮都是灰色的,不能够点击。
下单表单也尽量简单,购买数量只能是一个且不能够修改,送货地址和付款方式都使用用户默认设置,没有默认也能够不填,容许等订单提交后修改;只有第一个提交的订单发送给网站的订单子系统,其他用户提交订单后只能看到秒杀结束页面。
要作一个这样的秒杀系统,业务会分为两个阶段,第一个阶段是秒杀开始前某个时间到秒杀开始
, 这个阶段能够称之为准备阶段
,用户在准备阶段等待秒杀; 第二个阶段就是秒杀开始到全部参与秒杀的用户得到秒杀结果
, 这个就称为秒杀阶段
吧。
首先要有一个展现秒杀商品的页面, 在这个页面上作一个秒杀活动开始的倒计时, 在准备阶段内用户会陆续打开这个秒杀的页面, 而且可能不停的刷新页面
。这里须要考虑两个问题:
第一个是秒杀页面的展现
咱们知道一个html页面仍是比较大的,即便作了压缩,http头和内容的大小也可能高达数十K,加上其余的css, js,图片等资源
,若是同时有几千万人参与一个商品的抢购,通常机房带宽也就只有1G~10G,网络带宽就极有可能成为瓶颈
,因此这个页面上各种静态资源首先应分开存放,而后放到cdn节点上分散压力
,因为CDN节点遍及全国各地,能缓冲掉绝大部分的压力,并且还比机房带宽便宜~
第二个是倒计时
出于性能缘由这个通常由js调用客户端本地时间,就有可能出现客户端时钟与服务器时钟不一致,另外服务器之间也是有可能出现时钟不一致。客户端与服务器时钟不一致能够采用客户端定时和服务器同步时间
,这里考虑一下性能问题,用于同步时间的接口因为不涉及到后端逻辑,只须要将当前web服务器的时间发送给客户端就能够了,所以速度很快
,就我之前测试的结果来看,一台标准的web服务器2W+QPS不会有问题,若是100W人同时刷,100W QPS也只须要50台web,一台硬件LB就能够了~,而且web服务器群是能够很容易的横向扩展的(LB+DNS轮询),这个接口能够只返回一小段json格式的数据,并且能够优化一下减小没必要要cookie和其余http头的信息,因此数据量不会很大,通常来讲网络不会成为瓶颈,即便成为瓶颈也能够考虑多机房专线连通,加智能DNS的解决方案
;web服务器之间时间不一样步能够采用统一时间服务器的方式,好比每隔1分钟全部参与秒杀活动的web服务器就与时间服务器作一次时间同步
。
浏览器层请求拦截
(1)产品层面,用户点击“查询”或者“购票”后,按钮置灰,禁止用户重复提交请求;
(2)JS层面,限制用户在x秒以内只能提交一次请求;
前端层的请求拦截,只能拦住小白用户(不过这是99%的用户哟),高端的程序员根本不吃这一套,写个for循环,直接调用你后端的http请求,怎么整?
(1)同一个uid,限制访问频度
,作页面缓存,x秒内到达站点层的请求,均返回同一页面
(2)同一个item的查询,例如手机车次
,作页面缓存,x秒内到达站点层的请求,均返回同一页面
如此限流,又有99%的流量会被拦截在站点层。
站点层的请求拦截,只能拦住普通程序员,高级黑客,假设他控制了10w台肉鸡(而且假设买票不须要实名认证),这下uid的限制不行了吧?怎么整?
(1)大哥,我是服务层,我清楚的知道小米只有1万部手机,我清楚的知道一列火车只有2000张车票,我透10w个请求去数据库有什么意义呢?对于写请求,作请求队列,每次只透过有限的写请求去数据层,若是均成功再放下一批,若是库存不够则队列里的写请求所有返回“已售完”
;
(2)对于读请求,还用说么?cache来抗
,无论是memcached仍是redis,单机抗个每秒10w应该都是没什么问题的;
如此限流,只有很是少的写请求,和很是少的读缓存mis的请求会透到数据层去,又有99.9%的请求被拦住了。
用户请求分发模块:使用Nginx或Apache将用户的请求分发到不一样的机器上。
用户请求预处理模块:判断商品是否是还有剩余来决定是否是要处理该请求。
用户请求处理模块:把经过预处理的请求封装成事务提交给数据库,并返回是否成功。
数据库接口模块:该模块是数据库的惟一接口,负责与数据库交互,提供RPC接口供查询是否秒杀结束、剩余数量等信息。
用户请求预处理模块
通过HTTP服务器的分发后,单个服务器的负载相对低了一些,但总量依然可能很大,若是后台商品已经被秒杀完毕,那么直接给后来的请求返回秒杀失败便可,没必要再进一步发送事务了。
初始容量固定的阻塞队列
,咱们能够用来做为数据库模块成功竞拍的队列,好比有10个商品,那么咱们就设定一个10大小的数组队列。CAS原语无锁队列实现,是一个异步队列
,入队的速度很快,出队进行了加锁,性能稍慢。LinkedBlockingQueue也是阻塞的队列,入队和出队都用了加锁
,当队空的时候线程会暂时阻塞。
因为咱们的系统入队需求要远大于出队需求
,通常不会出现队空的状况,因此咱们能够选择ConcurrentLinkedQueue来做为咱们的请求队列实现