• 商城秒杀活动要点


    商城秒杀的特性:

    1、定时秒杀。即商品在秒杀时间点之前是不能进行购买下单。业务较简单。

    2、秒杀前用户会频繁刷新秒杀页面。

    3、秒杀持续时间短、瞬时访问流量高。

    4、同一用户/IP禁止秒杀多次。

    秒杀系统设计要点:

    1、将秒杀系统独立部署,甚至使用独立域名,使其与原有网站完全隔离。主要防止秒杀对现有网站业务造成冲击。

    2、页面静态化:将活动页面上的所有可以静态的元素全部静态化,并尽量减少动态元素。通过CDN来抗峰值。

    3、禁止重复提交:用户提交之后按钮置灰,禁止重复提交 用户限流:在某一时间段内只允许用户提交一次请求,比如可以采取IP限流

    4、秒杀提交地址在秒杀开始的时候才能得到。

    5、使用缓存、队列。将秒杀数量提前写入redis中。接收到请求时,直接递减redis中总数量,如果下单数量为0则请求直接返回。下单过程失败则增加redis中总数量。

    秒杀架构设计

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

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

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

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

    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秒之内只能提交一次请求;

    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%的请求被拦住了。

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

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

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

    4. 数据库接口模块:该模块是数据库的唯一接口,负责与数据库交互,提供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();
            }
        }
      }
  • 相关阅读:
    swoole中退出、异常与错误的处理笔记
    centos6.9+lnmp1.5环境部署swoole记录
    linux递归查找文件内容并替换
    使用FFMPEG 压缩png图片 与tinypng压缩结果对比
    XAMPP安装PHP_GMP
    Yii2 输出图片相关
    (xampp)lampp 下配置https(ssl)自签双向认证以后 apache无法启动解决方案
    PHP正则表达式匹配俄文字符
    windows 7 php 7.1 命令行 执行 中文文件名 的PHP文件
    centos6.9 安装完xampp 7.2.0后,执行/opt/lampp/lampp报错
  • 原文地址:https://www.cnblogs.com/shenjianjun/p/9205101.html
Copyright © 2020-2023  润新知