电商秒杀架构分析

        请尊重个人劳动成果,转载注明出处,谢谢!http://blog.csdn.net/u013322876/article/details/51121138

说到高并发,其实我们中国互联网人最有发言权。中国人口最多,而且特有的社会结构恰好是互联网精神发挥的沃土,所以互联网行业在中国的迅猛发展也属情理之中。而中国现实社会中的地域发展不平衡、产业结构过度区域化、交通不够便利及小企业信息不畅等一道道壁垒,反而成就了电商的独特的商业空间。

伴随着电商的蓬勃发展,抢购和秒杀活动作为营销的重要手段,也对电商系统提出了越来越高的要求。当双十一已经从一个简单的噱头演变为可以让全民熬夜万众秒杀的头条事件时,电商系统所面对的高并发压力绝不亚于春节的微信红包系统。

技术干货(2 of 5):电商秒杀架构分析

今天,我们就从实战的角度,对电商的抢购秒杀系统做一次深入彻底的解析:

1 秒杀业务分析

  1. 正常电子商务流程

    (1)查询商品;(2)创建订单;(3)扣减库存;(4)更新订单;(5)付款;(6)卖家发货

  2. 秒杀业务的特性

    (1)低廉价格;(2)大幅推广;(3)瞬时售空;(4)一般是定时上架;(5)时间短、瞬时并发量高;

2 秒杀技术挑战

假设某网站秒杀活动只推出一件商品,预计会吸引1万人参加活动,也就说最大并发请求数是10000,秒杀系统需要面对的技术挑战有:

  1. 对现有网站业务造成冲击

    秒杀活动只是网站营销的一个附加活动,这个活动具有时间短,并发访问量大的特点,如果和网站原有应用部署在一起,必然会对现有业务造成冲击,稍有不慎可能导致整个网站瘫痪。

    解决方案:将秒杀系统独立部署,甚至使用独立域名,使其与网站完全隔离

  2. 高并发下的应用、数据库负载

    用户在秒杀开始前,通过不停刷新浏览器页面以保证不会错过秒杀,这些请求如果按照一般的网站应用架构,访问应用服务器、连接数据库,会对应用服务器和数据库服务器造成负载压力。

    解决方案:重新设计秒杀商品页面,不使用网站原来的商品详细页面,页面内容静态化,用户请求不需要经过应用服务

  3. 突然增加的网络及服务器带宽

    假设商品页面大小200K(主要是商品图片大小),那么需要的网络和服务器带宽是2G(200K×10000),这些网络带宽是因为秒杀活动新增的,超过网站平时使用的带宽。

    解决方案:因为秒杀新增的网络带宽,必须和运营商重新购买或者租借。为了减轻网站服务器的压力,需要将秒杀商品页面缓存在CDN,同样需要和CDN服务商临时租借新增的出口带宽

  4. 直接下单

    秒杀的游戏规则是到了秒杀才能开始对商品下单购买,在此时间点之前,只能浏览商品信息,不能下单。而下单页面也是一个普通的URL,如果得到这个URL,不用等到秒杀开始就可以下单了。

    解决方案:为了避免用户直接访问下单页面URL,需要将改URL动态化,即使秒杀系统的开发者也无法在秒杀开始前访问下单页面的URL。办法是在下单页面URL加入由服务器端生成的随机数作为参数,在秒杀开始的时候才能得到

  5. 如何控制秒杀商品页面购买按钮的点亮

    购买按钮只有在秒杀开始的时候才能点亮,在此之前是灰色的。如果该页面是动态生成的,当然可以在服务器端构造响应页面输出,控制该按钮是灰色还 是点亮,但是为了减轻服务器端负载压力,更好地利用CDN、反向代理等性能优化手段,该页面被设计为静态页面,缓存在CDN、反向代理服务器上,甚至用户浏览器上。秒杀开始时,用户刷新页面,请求根本不会到达应用服务器。

    解决方案:使用JavaScript脚本控制,在秒杀商品静态页面中加入一个JavaScript文件引用,该JavaScript文件中包含 秒杀开始标志为否;当秒杀开始的时候生成一个新的JavaScript文件(文件名保持不变,只是内容不一样),更新秒杀开始标志为是,加入下单页面的URL及随机数参数(这个随机数只会产生一个,即所有人看到的URL都是同一个,服务器端可以用Redis这种分布式缓存服务器来保存随机数),并被用户浏览器加载,控制秒杀商品页面的展示。这个JavaScript文件的加载可以加上随机版本号(例如xx.js?v=32353823),这样就不会被浏览器、CDN和反向代理服务器缓存

    这个JavaScript文件非常小,即使每次浏览器刷新都访问JavaScript文件服务器也不会对服务器集群和网络带宽造成太大压力。

  6. 如何只允许第一个提交的订单被发送到订单子系统

    由于最终能够成功秒杀到商品的用户只有一个,因此需要在用户提交订单时,检查是否已经有订单提交。如果已经有订单提交成功,则需要更新 JavaScript文件,更新秒杀开始标志为否,购买按钮变灰。事实上,由于最终能够成功提交订单的用户只有一个,为了减轻下单页面服务器的负载压力, 可以控制进入下单页面的入口,只有少数用户能进入下单页面,其他用户直接进入秒杀结束页面。

    解决方案:假设下单服务器集群有10台服务器,每台服务器只接受最多10个下单请求。在还没有人提交订单成功之前,如果一台服务器已经有十单了,而有的一单都没处理,可能出现的用户体验不佳的场景是用户第一次点击购买按钮进入已结束页面,再刷新一下页面,有可能被一单都没有处理的服务器处理,进入了填写订单的页面,可以考虑通过cookie的方式来应对,符合一致性原则。当然可以采用最少连接的负载均衡算法,出现上述情况的概率大大降低。

  7. 如何进行下单前置检查

    如果超过10条,直接返回已结束页面给用户;

    如果未超过10条,则用户可进入填写订单及确认页面;

    已超过秒杀商品总数,返回已结束页面给用户;

    未超过秒杀商品总数,提交到子订单系统;

  • 检查全局已提交订单数目:

  • 下单服务器检查本机已处理的下单请求数目:

  • 秒杀一般是定时上架

    该功能实现方式很多。不过目前比较好的方式是:提前设定好商品的上架时间,用户可以在前台看到该商品,但是无法点击“立即购买”的按钮。但是需要考虑的是,有人可以绕过前端的限制,直接通过URL的方式发起购买,这就需要在前台商品页面,以及bug页面到后端的数据库,都要进行时钟同步。越在后端控制,安全性越高。

    定时秒杀的话,就要避免卖家在秒杀前对商品做编辑带来的不可预期的影响。这种特殊的变更需要多方面评估。一般禁止编辑,如需变更,可以走数据订正多的流程。

  • 减库存的操作

    有两种选择,一种是拍下减库存 另外一种是付款减库存;目前采用的“拍下减库存”的方式,拍下就是一瞬间的事,对用户体验会好些。

  • 库存会带来“超卖”的问题:售出数量多于库存数量

    由于库存并发更新的问题,导致在实际库存已经不足的情况下,库存依然在减,导致卖家的商品卖得件数超过秒杀的预期。

    方案:采用乐观锁

    update auction_auctions setquantity = #inQuantity#where auction_id = #itemId# and quantity = #dbQuantity#

  • 秒杀器的应对

    秒杀器一般下单个购买及其迅速,根据购买记录可以甄别出一部分。可以通过校验码达到一定的方法,这就要求校验码足够安全,不被破解,采用的方式有:秒杀专用验证码,电视公布验证码,秒杀答题

    3 秒杀架构原则

  1. 尽量将请求拦截在系统上游

    传统秒杀系统之所以挂,请求都压倒了后端数据层,数据读写锁冲突严重,并发高响应慢,几乎所有请求都超时,流量虽大,下单成功的有效流量甚小【一趟火车其实只有2000张票,200w个人来买,基本没有人能买成功,请求有效率为0】。

  2. 读多写少的常用多使用缓存

    这是一个典型的读多写少的应用场景【一趟火车其实只有2000张票,200w个人来买,最多2000个人下单成功,其他人都是查询库存,写比例只有0.1%,读比例占99.9%】,非常适合使用缓存

4 秒杀架构设计

秒杀系统为秒杀而设计,不同于一般的网购行为,参与秒杀活动的用户更关心的是如何能快速刷新商品页面,在秒杀开始的时候抢先进入下单页面,而不是商品详情等用户体验细节,因此秒杀系统的页面设计应尽可能简单。

商品页面中的购买按钮只有在秒杀活动开始的时候才变亮,在此之前及秒杀商品卖出后,该按钮都是灰色的,不可以点击。

下单表单也尽可能简单,购买数量只能是一个且不可以修改,送货地址和付款方式都使用用户默认设置,没有默认也可以不填,允许等订单提交后修改;只有第一个提交的订单发送给网站的订单子系统,其余用户提交订单后只能看到秒杀结束页面。

要做一个这样的秒杀系统,业务会分为两个阶段,第一个阶段是秒杀开始前某个时间到秒杀开始, 这个阶段可以称之为准备阶段,用户在准备阶段等待秒杀; 第二个阶段就是秒杀开始到所有参与秒杀的用户获得秒杀结果, 这个就称为秒杀阶段吧。

4.1 前端层设计

首先要有一个展示秒杀商品的页面, 在这个页面上做一个秒杀活动开始的倒计时, 在准备阶段内用户会陆续打开这个秒杀的页面, 并且可能不停的刷新页面。这里需要考虑两个问题:

  1. 第一个是秒杀页面的展示

    我们知道一个html页面还是比较大的,即使做了压缩,http头和内容的大小也可能高达数十K,加上其他的css, js,图片等资源,如果同时有几千万人参与一个商品的抢购,一般机房带宽也就只有1G~10G,网络带宽就极有可能成为瓶颈,所以这个页面上各类静态资源首先应分开存放,然后放到cdn节点上分散压力,由于CDN节点遍布全国各地,能缓冲掉绝大部分的压力,而且还比机房带宽便宜~

  2. 第二个是倒计时

    出于性能原因这个一般由js调用客户端本地时间,就有可能出现客户端时钟与服务器时钟不一致,另外服务器之间也是有可能出现时钟不一致。客户端与服务器时钟不一致可以采用客户端定时和服务器同步时间,这里考虑一下性能问题,用于同步时间的接口由于不涉及到后端逻辑,只需要将当前web服务器的时间发送给客户端就可以了,因此速度很快,就我以前测试的结果来看,一台标准的web服务器2W+QPS不会有问题,如果100W人同时刷,100W QPS也只需要50台web,一台硬件LB就可以了~,并且web服务器群是可以很容易的横向扩展的(LB+DNS轮询),这个接口可以只返回一小段json格式的数据,而且可以优化一下减少不必要cookie和其他http头的信息,所以数据量不会很大,一般来说网络不会成为瓶颈,即使成为瓶颈也可以考虑多机房专线连通,加智能DNS的解决方案;web服务器之间时间不同步可以采用统一时间服务器的方式,比如每隔1分钟所有参与秒杀活动的web服务器就与时间服务器做一次时间同步

  3. 浏览器层请求拦截

    (1)产品层面,用户点击“查询”或者“购票”后,按钮置灰,禁止用户重复提交请求;

    (2)JS层面,限制用户在x秒之内只能提交一次请求;

4.2 站点层设计

前端层的请求拦截,只能拦住小白用户(不过这是99%的用户哟),高端的程序员根本不吃这一套,写个for循环,直接调用你后端的http请求,怎么整?

(1)同一个uid,限制访问频度,做页面缓存,x秒内到达站点层的请求,均返回同一页面

(2)同一个item的查询,例如手机车次,做页面缓存,x秒内到达站点层的请求,均返回同一页面

如此限流,又有99%的流量会被拦截在站点层。

4.3 服务层设计

站点层的请求拦截,只能拦住普通程序员,高级黑客,假设他控制了10w台肉鸡(并且假设买票不需要实名认证),这下uid的限制不行了吧?怎么整?

(1)大哥,我是服务层,我清楚的知道小米只有1万部手机,我清楚的知道一列火车只有2000张车票,我透10w个请求去数据库有什么意义呢?对于写请求,做请求队列,每次只透过有限的写请求去数据层,如果均成功再放下一批,如果库存不够则队列里的写请求全部返回“已售完”

(2)对于读请求,还用说么?cache来抗,不管是memcached还是redis,单机抗个每秒10w应该都是没什么问题的;

如此限流,只有非常少的写请求,和非常少的读缓存mis的请求会透到数据层去,又有99.9%的请求被拦住了。

  1. 用户请求分发模块:使用Nginx或Apache将用户的请求分发到不同的机器上。

  2. 用户请求预处理模块:判断商品是不是还有剩余来决定是不是要处理该请求。

  3. 用户请求处理模块:把通过预处理的请求封装成事务提交给数据库,并返回是否成功。

  4. 数据库接口模块:该模块是数据库的唯一接口,负责与数据库交互,提供RPC接口供查询是否秒杀结束、剩余数量等信息。

  • 用户请求预处理模块

    经过HTTP服务器的分发后,单个服务器的负载相对低了一些,但总量依然可能很大,如果后台商品已经被秒杀完毕,那么直接给后来的请求返回秒杀失败即可,不必再进一步发送事务了,示例代码可以如下所示:

  • Java的并发包提供了三个常用的并发队列实现,分别是:ConcurrentLinkedQueue 、 LinkedBlockingQueue 和 ArrayBlockingQueue。

  • ArrayBlockingQueue是初始容量固定的阻塞队列,我们可以用来作为数据库模块成功竞拍的队列,比如有10个商品,那么我们就设定一个10大小的数组队列。

  • ConcurrentLinkedQueue使用的是CAS原语无锁队列实现,是一个异步队列,入队的速度很快,出队进行了加锁,性能稍慢。

  • LinkedBlockingQueue也是阻塞的队列,入队和出队都用了加锁,当队空的时候线程会暂时阻塞。

  • 由于我们的系统入队需求要远大于出队需求,一般不会出现队空的情况,所以我们可以选择ConcurrentLinkedQueue来作为我们的请求队列实现:

    • 并发队列的选择

  • 用户请求模块

  • package seckill;import org.apache.http.HttpRequest;public class Processor { /**

    * 发送秒杀事务到数据库队列.

    */

    public static void kill(BidInfo info) {

    DB.bids.add(info);

    } public static void process() {

    BidInfo info = new BidInfo(RequestQueue.queue.poll()); if (info != null) {

    kill(info);

    }

    }

    }class BidInfo {

    BidInfo(HttpRequest request) { // Do something.

    }

    }

  • 数据库模块

    数据库主要是使用一个ArrayBlockingQueue来暂存有可能成功的用户请求。

    package seckill;import java.util.concurrent.ArrayBlockingQueue;/**

    * DB应该是数据库的唯一接口.

    */public class DB { public static int count = 10; public static ArrayBlockingQueue<BidInfo> bids = new ArrayBlockingQueue<BidInfo>(10); public static boolean checkReminds() { // TODO

    return true;

    } // 单线程操作

    public static void bid() {

    BidInfo info = bids.poll(); while (count-- > 0) { // insert into table Bids values(item_id, user_id, bid_date, other)

    // select count(id) from Bids where item_id = ?

    // 如果数据库商品数量大约总数,则标志秒杀已完成,设置标志位reminds = false.

    info = bids.poll();

    }

    }

    }

4.4 数据库设计

4.4.1 基本概念

概念一“单库”

概念二“分片”

分片解决的是“数据量太大”的问题,也就是通常说的“水平切分”。一旦引入分片,势必有“数据路由”的概念,哪个数据访问哪个库。路由规则通常有3种方法:

  1. 范围:range

    优点:简单,容易扩展

    缺点:各库压力不均(新号段更活跃)

  2. 哈希:hash 【大部分互联网公司采用的方案二:哈希分库,哈希路由】

    优点:简单,数据均衡,负载均匀

    缺点:迁移麻烦(2库扩3库数据要迁移)

  3. 路由服务:router-config-server

    优点:灵活性强,业务与路由算法解耦

    缺点:每次访问数据库前多一次查询

概念三“分组”

分组解决“可用性”问题,分组通常通过主从复制的方式实现。

互联网公司数据库实际软件架构是:又分片,又分组(如下图)

4.4.2 设计思路

数据库软件架构师平时设计些什么东西呢?至少要考虑以下四点:

  1. 如何保证数据可用性;

  2. 如何提高数据库读性能(大部分应用读多写少,读会先成为瓶颈);

  3. 如何保证一致性;

  4. 如何提高扩展性;

  • 1. 如何保证数据的可用性?

    解决可用性问题的思路是=>冗余

    如何保证站点的可用性?复制站点,冗余站点

    如何保证服务的可用性?复制服务,冗余服务

    如何保证数据的可用性?复制数据,冗余数据

    数据的冗余,会带来一个副作用=>引发一致性问题(先不说一致性问题,先说可用性)

  • 2. 如何保证数据库“读”高可用?

    冗余读库

    冗余读库带来的副作用?读写有延时,可能不一致

    上面这个图是很多互联网公司MySQL的架构,写仍然是单点,不能保证写高可用。

  • 3. 如何保证数据库“写”高可用?

    冗余写库

    采用双主互备的方式,可以冗余写库带来的副作用?双写同步,数据可能冲突(例如“自增id”同步冲突),如何解决同步冲突,有两种常见解决方案:

  1. 两个写库使用不同的初始值,相同的步长来增加id:1写库的id为0,2,4,6...;2写库的id为1,3,5,7...;

  2. 不使用数据的id,业务层自己生成唯一的id,保证数据不冲突;

实际中没有使用上述两种架构来做读写的“高可用”,采用的是“双主当主从用”的方式

仍是双主,但只有一个主提供服务(读+写),另一个主是“shadow-master”,只用来保证高可用,平时不提供服务。 master挂了,shadow-master顶上(vip漂移,对业务层透明,不需要人工介入)。这种方式的好处:

  1. 读写没有延时;

  2. 读写高可用;

不足:

  1. 不能通过加从库的方式扩展读性能;

  2. 资源利用率为50%,一台冗余主没有提供服务;

那如何提高读性能呢?进入第二个话题,如何提供读性能。

  • 4. 如何扩展读性能

    提高读性能的方式大致有三种,第一种是建立索引。这种方式不展开,要提到的一点是,不同的库可以建立不同的索引

    写库不建立索引;

    线上读库建立线上访问索引,例如uid;

    线下读库建立线下访问索引,例如time;

    第二种扩充读性能的方式是,增加从库,这种方法大家用的比较多,但是,存在两个缺点:

    实际中没有采用这种方法提高数据库读性能(没有从库),采用的是增加缓存。常见的缓存架构如下:

    上游是业务应用,下游是主库,从库(读写分离),缓存

    实际的玩法:服务+数据库+缓存一套

    业务层不直接面向db和cache,服务层屏蔽了底层db、cache的复杂性。为什么要引入服务层,今天不展开,采用了“服务+数据库+缓存一套”的方式提供数据访问,用cache提高读性能

    不管采用主从的方式扩展读性能,还是缓存的方式扩展读性能,数据都要复制多份(主+从,db+cache),一定会引发一致性问题

  1. 从库越多,同步越慢;

  2. 同步越慢,数据不一致窗口越大(不一致后面说,还是先说读性能的提高);

  • 5. 如何保证一致性?

    主从数据库的一致性,通常有两种解决方案:

    1. 中间件

    如果某一个key有写操作,在不一致时间窗口内,中间件会将这个key的读操作也路由到主库上。这个方案的缺点是,数据库中间件的门槛较高(百度,腾讯,阿里,360等一些公司有)。

    2. 强制读主

    上面实际用的“双主当主从用”的架构,不存在主从不一致的问题

    第二类不一致,是db与缓存间的不一致

    常见的缓存架构如上,此时写操作的顺序是:

    (1)淘汰cache;

    (2)写数据库;

    读操作的顺序是:

    (1)读cache,如果cache hit则返回;

    (2)如果cache miss,则读从库;

    (3)读从库后,将数据放回cache;

    在一些异常时序情况下,有可能从【从库读到旧数据(同步还没有完成),旧数据入cache后】,数据会长期不一致。解决办法是“缓存双淘汰”,写操作时序升级为:

    (1)淘汰cache;

    (2)写数据库;

    (3)在经验“主从同步延时窗口时间”后,再次发起一个异步淘汰cache的请求;

    这样,即使有脏数据如cache,一个小的时间窗口之后,脏数据还是会被淘汰。带来的代价是,多引入一次读miss(成本可以忽略)。

    除此之外,最佳实践之一是:建议为所有cache中的item设置一个超时时间

  • 6. 如何提高数据库的扩展性?

    原来用hash的方式路由,分为2个库,数据量还是太大,要分为3个库,势必需要进行数据迁移,有一个很帅气的“数据库秒级扩容”方案。

    如何秒级扩容?

    首先,我们不做2库变3库的扩容,我们做2库变4库(库加倍)的扩容(未来4->8->16)

    服务+数据库是一套(省去了缓存),数据库采用“双主”的模式

    扩容步骤:

    第一步,将一个主库提升;

    第二步,修改配置,2库变4库(原来MOD2,现在配置修改后MOD4),扩容完成;

    原MOD2为偶的部分,现在会MOD4余0或者2;原MOD2为奇的部分,现在会MOD4余1或者3;数据不需要迁移,同时,双主互相同步,一遍是余0,一边余2,两边数据同步也不会冲突,秒级完成扩容!

    最后,要做一些收尾工作:

    这样,秒级别内,我们就完成了2库变4库的扩展。

    1. 将旧的双主同步解除;

    2. 增加新的双主(双主是保证可用性的,shadow-master平时不提供服务);

    3. 删除多余的数据(余0的主,可以将余2的数据删除掉);

    5 大并发带来的挑战(详见:http://blog.csdn.net/liuhaiabc/article/details/52757375)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值