PHP之秒杀设计

前言

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

课程目标

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

原理知识介绍

  • 减而治之(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

高性能的查库存服务实现

  • 在这里插入图片描述

基本需求实现总结

#### 链路如

何实现漏斗式流量

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • CDN原理:减少读的压力,比如订单详情页 nginx限流:当请求到了服务端之后,怎么做过载保护 异步队列:流量到达...
    Amy1234567阅读 349评论 0 2
  • 之前一直准备写一篇关于秒杀系统设计的文章,但是因为涉及到的东西还是挺多的,拖延症发作一直没抽空写,最近闲了就把这个...
    monkey01阅读 36,640评论 2 68
  • 什么是秒杀 秒杀场景一般会在电商网站举行一些活动或者节假日在12306网站上抢票时遇到。对于电商网站中一些稀缺或者...
    java456阅读 644评论 1 8
  • 很多的电商平台,在节假日如双十一,618等都会有商品描述的活动,今天和大家讨论一下,如何设计一个秒杀系统。 什么是...
    时之令阅读 19,570评论 0 33
  • 搞清楚秒杀的关键问题所在,有哪些解决办法。 知识要点架构设计原理如何最大程度分流减压如何抵挡突发大流量根据业务场景...
    与诗小睡阅读 259评论 0 0