美文网首页
PHP之秒杀设计

PHP之秒杀设计

作者: 东东锵 | 来源:发表于2021-01-24 15:51 被阅读0次

    前言

    • 源自慕课网皮奇 秒杀设计课程 所做的笔记

    课程目标

    • 掌握秒杀系统的核心实现
    • 系统高可用的方法论学习
    • 高并发场景的通用解决思路学习

    原理知识介绍

    • 减而治之(CDN原理/nginx限流/异步队形)
    • 分而治之(nginx负载均衡)

    特征与难点分析

    • 特征
      1 写强一致性(eg:商品超卖)
      2 读弱一致性(eg: 12306抢票 显示有抢不到)
    • 难点
      1 极致性能的实现(并发量高提高单服务)
      2 高可用的保证

    秒杀系统核心实现

    • 极致性能的读服务实现
    • 极致性能的写服务器(eg 扣库存 创建订单)
    • 极致性能的派对进度查询(eg:12306 抢票后查询排队)
    • 链路流量优化如何做

    兜底-高可用

    • 高可用的标准(4个9/3个9)
    • 请求链路中每层高可用的实现原理
    • 限流,一键降级,自动降级的实现

    正文

    压测工具的使用

    • ab
      1 安装
    yum -y install httpd-tools # 安装
    ab -V 查看是否安装成功
    

    2 使用
    检测接口最大的qps

    ab -n100 -c 10 http://xxx #  其中-c  几个并发   -n 是访问多少次   反馈为
    Requests per second:101.15[#/sec](mean)
    
    eg 在这里插入图片描述
    在这里插入图片描述

    nginx 限流配置

    • 按连接数限速,即并发数(ngx_http+limit_conn_module )
    • 按请求速率限速,按照 ip 限制单位时间内的请求数(ngx_http_limit_req_module)

    • 具体配置
    • 创建规则 : limit_req_zone $binary_remote_addr zone=mylimit:10m rate=1r/s (10M 是内存 1r/s 是一个请求每秒)
    • 应用规则: limit_req zone=mylimit burst=1 nodelay (burst 请求突发流量 保存缓存 nodelay 瞬间处理 减少排队)
    • eg


      创建规则
    应用规则

    限流算法与cdn原理

    限流算法

    在这里插入图片描述
    在这里插入图片描述
    • 两个算法不同之处 是令牌桶算法可以应对突发流量
    • 限流算法介绍-计数器
      1 单位时间计数器计数即可 一般在应用城区中写的较多

    CDN介绍

    • CDN 内容分发网络(Content Delivery Network)
    • 缩短访问路径,减少源站压力,提高内容相应速度
    • 为源站提供安全保护
    • 原理 eg:
    • 在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述

    大型网站架构

    在这里插入图片描述

    nginx 负载均衡算法介绍

    • Round-robin(轮循)

    • Weight-round-robin(推荐:带权轮循):(根据服务器性能的不同转发到不用服务器)


      在这里插入图片描述
    • Ip-hash(Ip 哈希)

    消息队列-介绍

    • 消息队列实际为链表,头插尾出,高并发下容易堵塞,为避免消息丢失,可通过写入实时消息队列进行延时处理
    • 现实生活中的应用:海底捞排队
      1 实时队列
      2 延时队列

      eg: 在这里插入图片描述

    消息队列的作用

    • 提高请求相应速度,如:创建订单后的流程,发push ,短信提醒等
    • 瞬间高并发下,可以起到削峰,如 双十一0点并发创建订单
    • 延时队列 时间维度任务触发 如 发货提醒

    秒杀系统的分析

    使用场景

    • 商城活动抢购,优惠券,定时抢购 (有效写 100+ 并发抢 1W+
    • 小米商城手机抢购 (有效写1W+ 并发抢100W+
    • 12306的抢票(有效写1w+ 并发抢100w+
    • 天猫双十一凌晨促销(有效写10W+ 并发抢 1000W+

    秒杀系统-特点

    • 抢购人数远多于库存,读写并发巨大
    • 库存少,有效写少
    • 写需强一致性,商品部能超卖
    • 读一致性要求并不高
    • eg: 在这里插入图片描述

    秒杀系统的重难点

    • 稳定性难
      1 高并发下,某个小依赖可能造成雪崩
      2 流量预期难精确,过高也造成雪崩
      3 分布式集群,机器多,出故障的概率高
    • 准确性难
      1 库存,抢购成功数,创建订单之前的一致性
    • 高性能难
      1 有限成本下需要做到极致的性能

    秒杀系统的架构原则

    • 稳定性
      1 减少第三方依赖,同时自身部署也需要做到隔离
      2 压测,降级,限流方案,确保核心服务可用
      3 需健康读检查机制,整个链路避免单点(剔除故障机器 分布式集群中)
    • 高性能
      1 缩短单请求访问路径,减少IO
      2 减少接口数,降低吞吐数据量,请求册数较少
    • 目标
      1 满足高并发且高可用的系统

    秒杀系统的核心实现

    • 秒杀服务核心怎么设计
      1 满足基本需求(扣库存。查库存、排队进度。查询订单、创建订单、支付订单),单服务的极致性能
      2 请求链路流量优化,从客户端到服务端每层优化
      3 稳定性建设

    秒杀系统基本需求分析

    • 扣库存方案


      在这里插入图片描述
    • 预扣库存方案实现
      1 先扣除库存,然后再创建订单支付
      2 10分钟内不支付则取消订单,避免不支付库存买不去的问题

    极致性能的扣库存服务如何实现

    在这里插入图片描述

    扣库存分布式实现方案

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    • 如何实现
      1 初始化库存到本地库存
      2 本地减库存,成功则进行统一减库存,失败则返回
      3 统一减库存成功则写入Mysql,异步创建订单
      4 告知用户抢购成功

    扣库存代码演示(api.php、base.php)

    • base.php
    <?php
    
    class Base
    {
        static $redisObj;
    
        static function conRedis($config = array())
        {
            if (self::$redisObj) return self::$redisObj;
            self::$redisObj = new \Redis();
            self::$redisObj->connect("127.0.0.1", 6379);
            return self::$redisObj;
        }
    
        static function output($data = array(), $errNo = 0, $errMsg = 'ok')
        {
            $res['errno'] = $errNo;
            $res['errmsg'] = $errMsg;
            $res['data'] = $data;
            echo json_encode($res);exit();
        }
    }
    
    ?>
    
    • api.php
    <?php
    include('base.php');
    class Api extends Base
    {
        //共享信息,存储在redis中,以hash表的形式存储,%s变量代表的是商品id
        static $userId;
        static $productId;
    
        static $REDIS_REMOTE_HT_KEY         = "product_%s";     //共享信息key
        static $REDIS_REMOTE_TOTAL_COUNT    = "total_count";    //商品总库存
        static $REDIS_REMOTE_USE_COUNT      = "used_count";     //已售库存
        static $REDIS_REMOTE_QUEUE          = "c_order_queue";  //创建订单队列
    
        static $APCU_LOCAL_STOCK    = "apcu_stock_%s";       //总共剩余库存
    
        static $APCU_LOCAL_USE      = "apcu_stock_use_%s";   //本地已售多少
        static $APCU_LOCAL_COUNT    = "apcu_total_count_%s"; //本地分库存分摊总数
    
        public function __construct($productId, $userId)
        {
            self::$REDIS_REMOTE_HT_KEY  = sprintf(self::$REDIS_REMOTE_HT_KEY, $productId);
            self::$APCU_LOCAL_STOCK     = sprintf(self::$APCU_LOCAL_STOCK, $productId);
            self::$APCU_LOCAL_USE       = sprintf(self::$APCU_LOCAL_USE, $productId);
            self::$APCU_LOCAL_COUNT     = sprintf(self::$APCU_LOCAL_COUNT, $productId);
            self::$APCU_LOCAL_COUNT     = sprintf(self::$APCU_LOCAL_COUNT, $productId);
            self::$userId               = $userId;
            self::$productId            = $productId;
        }
        static  function clear(){
        apcu_delete(self::$APCU_LOCAL_STOCK);
        apcu_delete(self::$APCU_LOCAL_USE);
        apcu_delete(self::$APCU_LOCAL_COUNT);
            
        }
        /*查剩余库存*/
        static function getStock()
        {
        $stockNum = apcu_fetch(self::$APCU_LOCAL_STOCK);
            if ($stockNum === false) {
                $stockNum = self::initStock();
            }
            self::output(['stock_num' => $stockNum]);
        }
    
        /*抢购-减库存*/
        static function buy()
        {
            $localStockNum = apcu_fetch(self::$APCU_LOCAL_COUNT);
            if ($localStockNum === false) {
                $localStockNum = self::init();
            }
    
            $localUse = apcu_inc(self::$APCU_LOCAL_USE);//本已卖 + 1
            if ($localUse > $localStockNum) {//抢购失败 大部分流量在此被拦截
            echo 1;
                self::output([], -1, '该商品已售完');
            }
    
            //同步已售库存 + 1;
            if (!self::incUseCount()) {//改失败,返回商品已售完
                self::output([], -1, '该商品已售完');
            }
    
            //写入创建订单队列
            self::conRedis()->lPush(self::$REDIS_REMOTE_QUEUE, json_encode(['user_id' => self::$userId, 'product_id' => self::$productId]));
            //返回抢购成功
            self::output([], 0, '抢购成功,请从订单中心查看订单');
        }
    
        /*创建订单*/
        /*查询订单*/
        /*总剩余库存同步本地,定时执行就可以*/
        static function sync()
        {
        $data = self::conRedis()->hMGet(self::$REDIS_REMOTE_HT_KEY, [self::$REDIS_REMOTE_TOTAL_COUNT, self::$REDIS_REMOTE_USE_COUNT]);
            $num = $data['total_count'] - $data["used_count"];
            apcu_add(self::$APCU_LOCAL_STOCK, $num);
            self::output([], 0, '同步库存成功');
        }
        /*私有方法*/
        //库存同步
        private static function incUseCount()
        {
            //同步远端库存时,需要经过lua脚本,保证不会出现超卖现象
            $script = <<<eof
                local key = KEYS[1]
                local field1 = KEYS[2]
                local field2 = KEYS[3]
                local field1_val = redis.call('hget', key, field1)
                local field2_val = redis.call('hget', key, field2)
                if(field1_val>field2_val) then
                    return redis.call('HINCRBY', key, field2,1)
                end
                return 0
    eof;
            return self::conRedis()->eval($script,[self::$REDIS_REMOTE_HT_KEY,  self::$REDIS_REMOTE_TOTAL_COUNT, self::$REDIS_REMOTE_USE_COUNT] , 3);
        }
        /*初始化本地数据*/
        private static function init()
        {
            apcu_add(self::$APCU_LOCAL_COUNT, 150);
            apcu_add(self::$APCU_LOCAL_USE, 0);
        }
        static  function initStock(){
            $data = self::conRedis()->hMGet(self::$REDIS_REMOTE_HT_KEY, [self::$REDIS_REMOTE_TOTAL_COUNT, self::$REDIS_REMOTE_USE_COUNT]);
            $num = $data['total_count']- $data["used_count"];
            apcu_add(self::$APCU_LOCAL_STOCK, $num);
            return $num;
        }
    
    }
    
    try{
    $act = $_GET['act'];
    $product_id = $_GET['product_id'];
    $user_id = $_GET['user_id'];
    
    $obj = new Api($product_id, $user_id);
    if (method_exists($obj, $act)) {
        $obj::$act();
        die;
    }
    echo 'method_error!';
    } catch (\Exception $e) {
        echo 'exception_error!';
        var_dump($e);
    }
    

    商品信息页及抢购进度查询实现

    • 创建、支付订单服务
      1 与扣库存服务隔离


      在这里插入图片描述

      2 用户收到抢购成功,页面跳转到订单中心去支付订单

    • 读商品信息页
      1 与库存服务隔离
      2 商品库一主多从提高读能力
      3 页面静态化+缓存+db实现即可
    • 排队进度查看


      1

    高性能的查库存服务实现

    • 在这里插入图片描述

    基本需求实现总结

    #### 链路如

    何实现漏斗式流量

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    相关文章

      网友评论

          本文标题:PHP之秒杀设计

          本文链接:https://www.haomeiwen.com/subject/zkcizktx.html