秒杀

1、应对并发访问量大,部署和前端方案:

  1. 秒杀系统独立部署,独立域名;
  2. 秒杀前按钮置灰,不能发送请求
  3. 产品层面,用户点击“查询”或“购买”后,按钮置灰,禁止用户重复提交请求;
  4. js层面,限制用户在x秒内只能提交一次请求;
  5. 前端缓存,当用户一直刷新页面的时候,前端可以到浏览器里面获取缓存数据

2、带宽问题

  1. 加大宽带
  2. 图片或静态文件压缩 (原文件压缩或nginx输出时候压缩)
  3. CDN加速

3、大部分请求不会生成订单

接入层(nginx)漏桶限流,真正进入php和mysql等应用层流量极少,大多被过滤

4、超卖问题

 

 

 4.1、秒杀难点主流解决方案

1、队列

2、负载均衡

3、接入层限流 + 配置中心限流实现过载保护

 4.2、超卖问题主流解决方案

 4.2.1、mysql悲观锁

排它锁

4.2.2、mysql乐观锁 (MVCC多版本并发控制)

乐观锁在数据库上的实现完全是逻辑的,数据库本身不提供支持,而是需要开发者自己来实现。

<?php 

$version = mysqlquery("select version from goods");

 //##其他逻辑省略


 mysqlquery("update goodsset stock=stock-1 ,version =version +1 where id=100 and version = $version");

注意:更新失败重新获取版本

乐观锁不锁数据,而是通过控制版本号实现

4.2.3、php+队列(队列:redis list、redis stream、rabbitmq)

序列化,不会产生多线程问题

4.2.4、php + redis 分布式锁,以及分布式锁主流优化方案

分布式锁示例代码:

$expire = 10;//有效期10秒
$key = 'lock';//key
$value = time() + $expire;//锁的值 = Unix时间戳 + 锁的有效期
$status = true;
while($status)
{
  $lock = $redis->setnx($key, $value);
  if(empty($lock)){
     $value = $redis->get($key);
     if($value < time()){
        $redis->del($key);
      }
}
else{ $status = false; //下步业务操作.... } }

4.2.5、php + redis乐观锁 redis watch

<?php
header("content-type:text/html;charset=utf-8");
$redis      = new redis();
$result     = $redis->connect('127.0.0.1', 6379);
$mywatchkey = $redis->get("");
$rob_total  = 10; //抢购数量
if ($mywatchkey < $rob_total) {
    $redis->watch("mywatchkey"); //声明一个乐观锁
$redis->multi(); //redis事务开始

//设置延迟,方便测试效果。
    sleep(5);
//插入抢购数据
    $redis->hSet("mywatchlist", "user_id_" . mt_rand(1, 9999), time());
    $redis->set("mywatchkey", $mywatchkey + 1);//乐观锁的版本号+1
$rob_result = $redis->exec(); //redis事务提交
if ($rob_result) {
        $mywatchlist = $redis->hGetAll("mywatchlist");
        echo "抢购成功!";
        echo "剩余数量:" . ($rob_total - $mywatchkey - 1) . "";
        echo "用户列表:";
        var_dump($mywatchlist);
    } else {
        echo "手气不好,再抢购!";
        exit;
    }
}

优点:

1、首先选用内存数据来抢购速度极快

2、速度快并发自然不是问题

3、使用悲观锁,会迅速增加系统资源

4、比队列强得多,队列会使内存数据瞬间爆棚

5、使用乐观锁,达到综合需求

 

4.2.6、nginx + lua +redis (【watch + 事务】)实现乐观锁 (推荐

 

 

 

客户端→代理层→应用层→数据库→压力测试:
客户端 90% 静态 HTML+10% 动态 JS;配合 CDN 做好缓存工作。
接入层专注于过滤和限流。
应用层利用缓存+队列+分布式+分库分表处理好订单。
做好数据的预估,隔离,合并。

上线之前记得进行压力测试。

 

nginx+lua+redis乐观锁代码:

--获取get或post参数--------------------

local request_method = ngx.var.request_method
local args = nil
local param = nil
--获取参数的值
--获取秒杀下单的用户id
if "GET" == request_method then
    args = ngx.req.get_uri_args()
elseif "POST" == request_method then
    ngx.req.read_body()
    args = ngx.req.get_post_args()
end
user_id = args["user_id"]
--用户身份判断--省略
--用户能否下单--省略

--关闭redis的函数--------------------

local function close_redis(redis_instance)
    if not redis_instance then
        return
    end
    local ok,err = redis_instance:close();
    if not ok then
        ngx.say("close redis error : ",err);
    end
end

--引入cjson类--------------------
--local cjson = require "cjson"


--连接redis--------------------

local redis = require("resty.redis");
--local redis = require "redis"
-- 创建一个redis对象实例。在失败,返回nil和描述错误的字符串的情况下
local redis_instance = redis:new();
--设置后续操作的超时(以毫秒为单位)保护,包括connect方法
redis_instance:set_timeout(1000)
--建立连接
local ip = '127.0.0.1'
local port = 6379
--尝试连接到redis服务器正在侦听的远程主机和端口
local ok,err = redis_instance:connect(ip,port)
if not ok then
    ngx.say("connect redis error : ",err)
    return close_redis(redis_instance);
end




-- 加载nginx—lua限流模块
local limit_req = require "resty.limit.req"
-- 这里设置rate=50个请求/每秒,漏桶桶容量设置为1000个请求
-- 因为模块中控制粒度为毫秒级别,所以可以做到毫秒级别的平滑处理
local lim, err = limit_req.new("my_limit_req_store", 50, 1000)
if not lim then
    ngx.log(ngx.ERR, "failed to instantiate a resty.limit.req object: ", err)
    return ngx.exit(501)
end

local key = ngx.var.binary_remote_addr
local delay, err = lim:incoming(key, true)


ngx.say("计算出来的延迟时间是:")
ngx.say(delay)

--if ( delay <0 or delay==nil ) then
    --return ngx.exit(502)
--end

--先死这个值为-1, 就是先不限流, 先测试下面的乐观锁代码。
--delay = -1

-- 1000以外的就溢出,回绝掉,比如100000个人来抢购,那么100000-1000的请求直接nginx回绝
if not delay then
    if err == "rejected" then
        return ngx.say("1000以外的就溢出")
        -- return ngx.exit(502)
    end
    ngx.log(ngx.ERR, "failed to limit req: ", err)
    return ngx.exit(502)
end

-- 计算出要等很久,比如要等10秒的, 也直接不要他等了。要买家直接回家吃饭去
if ( delay >10) then
    ngx.say("抢购超时")
    return
end

--先到redis里面添加sku_num键(参与秒杀的该商品的数量)
--并到redis里面添加watch_key键(用于做乐观锁之用)

local resp, err = redis_instance:get("sku_num")
resp = tonumber(resp)
ngx.say("数量:")
ngx.say(resp)
if (resp > 0) then
    --ngx.say("抢购成功")
    redis_instance:watch("watch_key");
    ngx.sleep(1)
    local ok, err = redis_instance:multi();
    local sku_num = tonumber(resp) - 1;
    ngx.say("goods_num:")
    ngx.say(sku_num)
    redis_instance:set("sku_num",sku_num);
    redis_instance:set("watch_key",1);
    ans, err = redis_instance:exec()
    ngx.say("ans:")
    ngx.say(ans)
    ngx.say(tostring(ans))
    ngx.say("--")
    if (tostring(ans) == "userdata: NULL") then
        ngx.say("抢购失败,慢一丁点")
        return
    else
        ngx.say("抢购成功")
        return
    end

else
    ngx.say("抢购失败,手慢了")
    return
end

--下面这行代码是进入正式下单;
ngx.exec('/create_order'); --注意这行代码前面不能执行ngx.say()



--[[
--每个用户限购1个,判断用户是否已经抢购过了的参考代码逻辑思路如下(具体过程略,前端缓存中也有这个类似的判断用于限制对后端的请求):

建一张用于保存已经抢购成功了的用户的redis哈希表

抢购前判断是否在该表中
local res, err = redis_instance:hmget("myhash", "user_id")
抢购成功则保存到该表
local res, err = redis_instance:hmset("myhash", "user_id", "1")
--]]

 

posted @ 2021-05-25 15:55  08ruanjian306  阅读(177)  评论(0)    收藏  举报