秒杀

1.秒杀的特点


  • 常见的场景比如10000人同时是抢购一个手机,比如12:00:00抢购,12:00:01活动就结束了

1.1 突然多了很多访问,可能导致原有的商城瘫痪

秒杀活动只是网站营销的一个附加活动,这个活动具有时间短,并发访问量大的特点,如果网站原有应用部署在一起,
必然会对现有业务造成冲击,稍有不慎可能导致整个网站瘫痪

解决方案:将秒杀系统独立部署,独立域名

前端如何优化:
首先要有一个展示秒杀商品的页面,在这个页面上做一个秒杀活动开始的倒计时,在准备阶段内用户会陆续
打开这个页面,并且可能不停的刷新页面,这里需要考虑这个问题:
1.秒杀前按钮是灰色的,不能发送请求,当然后端肯定也是要有判断;
2.产品层面,用户点击查询,或者购票后,按钮置灰,禁止用户重复提交请求;
3.jd层面,限制用户在x秒内只能提交一次请求

1.2 宽带问题

假设商品页面大小1M(主要是商品图片大小),那么10000个用户并发,需要的网络带宽是:10G(1M*10000)
这些网络带宽是因为秒杀活动新增的,超过网站平时使用带宽
解决方案:因为秒杀新增的网络带宽,必须和运营商重新购买或者租借,为了减轻网站服务器的压力,需要将秒杀商品
页面缓存在cdn,同样需要和cdn服务商临时租借出口带宽

1.3 有大部分请求不会生成订单

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

1.4 超卖问题:秒杀商品的数量是有限的

MyAnswer博客

假设库存只剩1件,现在2个人同时过来抢

if(库存数量 >= 下单数){
    可以购买						
   购买成功,然后把库存数量减少						
}else{
   不能购买
}

上面这个代码,在并发的场景是会造成超卖的

2. 超卖问题的行业主流解决方法:

  1. mysql悲观锁

  2. mysql乐观锁

  3. PHP+队列

  4. PHP+redis分布式锁,以及分布式锁的优化方案

  5. PHP+redis乐观锁redis watch

2.1 mysql悲观锁

悲观锁,正如其名,它指的对数据被外界(包括当前系统的其它事物,以及来自外部系统的事物处理)修改保持态度,因此在整个数据处理过程中将数据处于锁定状态,悲观锁的实现,往往依靠数据库提供的锁机制

锁住数据: SELECT * FROM employee WHERE id = 1 FOR UPDATE;

2.2 mysql乐观锁

乐观锁一般请求下数据不会造成冲突,所有在数据进行提交更新时才会对数据的冲突检测,如果没有冲突那就ok,如果出现冲突,返回错误信息并让用户决定如何去做

update items set quantity=quantity-1,version=version+1 where id=100 and version=#{version};						
$version = mysqlquery(SELECT VERSION FROM employee)
#这里写业务逻辑#省略
mysqlquery("UPDATE employee SET money = 1, VERSION=VERSION+1 WHERE VERSION=$version")
成功的哪个,mysql会返回更新成功
失败的哪个,mysql会返回更新失败
于是,php层面的话:
成功的哪个,mysql会返回更新成功,php就返回前端:恭喜你,抢购成功;
失败的哪个,mysql会返回更新失败,php就返回前端:抢购失败;

总结:
乐观锁不锁数据,而是通过版本号控制,会有不同结果放回给php,把决策权交给php

对比:
乐观锁:不需要锁数据,性能高于悲观锁

2.3 PHP+队列

序列化,不会产生多个线程之前的冲突


2.4 PHP+redis分布式锁,以及分布式锁主流优化分案

相当于是php线程锁,10000个抢购请求并发过来,有10000个线程,是同一时刻只会有一个线程在执行业务代码,其他线程都在死循环中等待

redis分布式锁与原理

127.0.0.1:6379> get name                                                                                                     
(nil)
127.0.0.1:6379> exists job                                                                                                  
(integer) 0
127.0.0.1:6379> setnx job 1                                                                                                  
(integer) 1
127.0.0.1:6379> setnx job test                                                                                              
(integer) 0
127.0.0.1:6379> get job                                                                                                      
"1"
127.0.0.1:6379>

可见setnx和set的区别,setnx只能1次,set可以无数次,redis分布式锁就是利用了这个

<?php
$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; //下步操作....
   }
}

10000个人同时进来这个代码,始终只有1个人在执行库存等业务操作,其他的都在死循环中等待锁的释放

优化方式:设置更多的锁,比如抢购20个商品,就可以设置20个锁,100000个人进来,就有20个线程是在执行业务逻辑的,其他的就在等待

2.5 PHP+redis乐观锁

<?php
header("content-type:text/html;charset=utf-8");
$redis = new redis();
$result = $redis->connect('127.0.0.1', 6379);
$mywatchkey = $redis->get("mywatchkey");
$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. 使用乐观锁,达到综合需求

2.6 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")
--]]


MyAnswer博客
请先登录后发表评论
  • 最新评论
  • 总共0条评论