秒杀设计

2020-03-30  本文已影响0人  AGEGG

一、原理

减而治之:

CDN原理
niginx限流
异步队列

分而治之:

niginx负载均衡

特征:

写强一致性
读弱一致性

难点:

极限性能的实现
高可用的保证

实现:

读/写服务,排队进度查询实现,链路流量优化

高可用:

标准
请求链路中每层高可用的实现原理
限流,一键降级,自动降级实现

压测工具安装

安装:yum -y install httpd-tools
检测是否成功ab -V

检测接口最大qps

//10个并发接口访问http://xxx 共访问100次
ab -n100 -c 10 http://xxx

Requests per second: 3673.55[#/sec](mean)

nginx 限流配置

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

//http模块创建规则
limit_req_zone $binary_remote_addr zone =mylimit:10m rate=1r/s;
创建一条请求速率,以用户的ip地址为key,名称为mylimit,10m内存,1条请求每秒

//应用规则
limit_req zone=mylimit burst=1 nodelay;
应用mylimit规则,burst=1,允许用户弹性超出1个连接数,nodelay:不阻塞后面用户连接

令牌桶算法

匀速生产令牌放入令牌桶
请求访问取走令牌桶令牌
若桶中无令牌返回503

漏桶算法

请求放入漏桶中,匀速请求,若漏桶满了则溢出返回503

计数器

单位时间计数器计数即可,一般在应用程序中写的多

CDN

CDN,内容分发网络(Content Delivery Newwork)
缩短访问路径,减少源站压力,提高内容响应速度
为源站提供安全保护

普通域名访问

gethostbyname("www.test.com");
gethostbyname{
生成查询DNS服务器的消息(域名,class,记录类型)
通过UDP协议向DNS服务器发送消息
接受DNS服务器返回的消息并读取ip地址返回
}
拿到ip地址访问服务器

DNS解析原理

任何一台DNS服务器都保留根域信息
上级DNS服务器保管着所有下级DNS服务器信息


image.png image.png image.png image.png

nginx负载均衡算法介绍

Round-robin(轮循)
Weight-round-robin(带权轮循)
Ip-hash(Ip哈希)


image.png

消息队列

消息队列实际为链表,头插尾出,高并发下容易发生堵塞,为避免消息丢失,可通过写入实时消息队列进行延时处理。
如:海底捞排队
实时队列:先进先出
延时队列:按发生时间排队,插入时查找需要插入的位置

作用:

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

总结

1.秒杀商品url加盐
2.单机redis集群(主从)
3.redis哨兵
4.nginx 负载均衡(带权轮循) 多php-fpm
5.php apcu拓展缓存,比redis快10倍
6.前端内容缓存,前端按钮防重复点击,前端点击验证码(防机器刷与削峰)
7.ip与uid限流,nginx ip限流
8.redis记录库存与抢购人,异步创建订单
9.redis+lua保证事务原子性
10.cdn页面静态化
11.RabbitMQ队列

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);
}

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();
    }
}

?>
php apcu本地缓存拓展比redis快


扣库存分布式:
1.初始化库存分摊
(设置库存分摊为定值,设置本地已售为0)
2.本地已售+1
若本地已售 大于 本地分摊 “商品已售完”
3.远端redis库同步库存
若超出则 “该商品已售完”
4.写入redis创建订单队列
上一篇 下一篇

猜你喜欢

热点阅读