商城秒杀的特性:
1、定时秒杀。即商品在秒杀时间点之前是不能进行购买下单。业务较简单。
2、秒杀前用户会频繁刷新秒杀页面。
3、秒杀持续时间短、瞬时访问流量高。
4、同一用户/IP禁止秒杀多次。
秒杀系统设计要点:
1、将秒杀系统独立部署,甚至使用独立域名,使其与原有网站完全隔离。主要防止秒杀对现有网站业务造成冲击。
2、页面静态化:将活动页面上的所有可以静态的元素全部静态化,并尽量减少动态元素。通过CDN来抗峰值。
3、禁止重复提交:用户提交之后按钮置灰,禁止重复提交 用户限流:在某一时间段内只允许用户提交一次请求,比如可以采取IP限流
4、秒杀提交地址在秒杀开始的时候才能得到。
5、使用缓存、队列。将秒杀数量提前写入redis中。接收到请求时,直接递减redis中总数量,如果下单数量为0则请求直接返回。下单过程失败则增加redis中总数量。
秒杀架构设计
秒杀系统为秒杀而设计,不同于一般的网购行为,参与秒杀活动的用户更关心的是如何能快速刷新商品页面,在秒杀开始的时候抢先进入下单页面,而不是商品详情等用户体验细节,因此秒杀系统的页面设计应尽可能简单。
商品页面中的购买按钮只有在秒杀活动开始的时候才变亮,在此之前及秒杀商品卖出后,该按钮都是灰色的,不可以点击。
下单表单也尽可能简单,购买数量只能是一个且不可以修改,送货地址和付款方式都使用用户默认设置,没有默认也可以不填,允许等订单提交后修改;只有第一个提交的订单发送给网站的订单子系统,其余用户提交订单后只能看到秒杀结束页面。
要做一个这样的秒杀系统,业务会分为两个阶段,第一个阶段是秒杀开始前某个时间到秒杀开始
, 这个阶段可以称之为准备阶段
,用户在准备阶段等待秒杀; 第二个阶段就是秒杀开始到所有参与秒杀的用户获得秒杀结果
, 这个就称为秒杀阶段
吧。
1 前端层设计
首先要有一个展示秒杀商品的页面, 在这个页面上做一个秒杀活动开始的倒计时, 在准备阶段内用户会陆续打开这个秒杀的页面, 并且可能不停的刷新页面
。这里需要考虑两个问题:
-
第一个是秒杀页面的展示
我们知道一个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秒之内只能提交一次请求;
2 站点层设计
前端层的请求拦截,只能拦住小白用户(不过这是99%的用户哟),高端的程序员根本不吃这一套,写个for循环,直接调用你后端的http请求,怎么整?
(1)同一个uid,限制访问频度
,做页面缓存,x秒内到达站点层的请求,均返回同一页面
(2)同一个item的查询,例如手机车次
,做页面缓存,x秒内到达站点层的请求,均返回同一页面
如此限流,又有99%的流量会被拦截在站点层。
3 服务层设计
站点层的请求拦截,只能拦住普通程序员,高级黑客,假设他控制了10w台肉鸡(并且假设买票不需要实名认证),这下uid的限制不行了吧?怎么整?
(1)大哥,我是服务层,我清楚的知道小米只有1万部手机,我清楚的知道一列火车只有2000张车票,我透10w个请求去数据库有什么意义呢?对于写请求,做请求队列,每次只透过有限的写请求去数据层,如果均成功再放下一批,如果库存不够则队列里的写请求全部返回“已售完”
;
(2)对于读请求,还用说么?cache来抗
,不管是memcached还是redis,单机抗个每秒10w应该都是没什么问题的;
如此限流,只有非常少的写请求,和非常少的读缓存mis的请求会透到数据层去,又有99.9%的请求被拦住了。
-
用户请求分发模块:使用Nginx或Apache将用户的请求分发到不同的机器上。
-
用户请求预处理模块:判断商品是不是还有剩余来决定是不是要处理该请求。
-
用户请求处理模块:把通过预处理的请求封装成事务提交给数据库,并返回是否成功。
-
数据库接口模块:该模块是数据库的唯一接口,负责与数据库交互,提供RPC接口供查询是否秒杀结束、剩余数量等信息。
-
用户请求预处理模块
经过HTTP服务器的分发后,单个服务器的负载相对低了一些,但总量依然可能很大,如果后台商品已经被秒杀完毕,那么直接给后来的请求返回秒杀失败即可,不必再进一步发送事务了,示例代码可以如下所示:
package seckill; import org.apache.http.HttpRequest; /** * 预处理阶段,把不必要的请求直接驳回,必要的请求添加到队列中进入下一阶段. */ public class PreProcessor { // 商品是否还有剩余 private static boolean reminds = true; private static void forbidden() { // Do something. } public static boolean checkReminds() { if (reminds) { // 远程检测是否还有剩余,该RPC接口应由数据库服务器提供,不必完全严格检查. if (!RPC.checkReminds()) { reminds = false; } } return reminds; } /** * 每一个HTTP请求都要经过该预处理. */ public static void preProcess(HttpRequest request) { if (checkReminds()) { // 一个并发的队列 RequestQueue.queue.add(request); } else { // 如果已经没有商品了,则直接驳回请求即可. forbidden(); } } }
- 并发队列的选择
Java的并发包提供了三个常用的并发队列实现,分别是:ConcurrentLinkedQueue 、 LinkedBlockingQueue 和 ArrayBlockingQueue。
ArrayBlockingQueue是
初始容量固定的阻塞队列
,我们可以用来作为数据库模块成功竞拍的队列,比如有10个商品,那么我们就设定一个10大小的数组队列。ConcurrentLinkedQueue使用的是
CAS原语无锁队列实现,是一个异步队列
,入队的速度很快,出队进行了加锁,性能稍慢。LinkedBlockingQueue也是
阻塞的队列,入队和出队都用了加锁
,当队空的时候线程会暂时阻塞。由于我们的系统
入队需求要远大于出队需求
,一般不会出现队空的情况,所以我们可以选择ConcurrentLinkedQueue来作为我们的请求队列实现:package seckill; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ConcurrentLinkedQueue; import org.apache.http.HttpRequest; public class RequestQueue { public static ConcurrentLinkedQueue<HttpRequest> queue = new ConcurrentLinkedQueue<HttpRequest>(); }
-
用户请求模块
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(); } } }