1.秒杀常见问题
常见的场景比如100000人在同一秒抢一个手机。比如12:00:00抢购, 12:00:01活动就结束了
1.1 突然多了很多访问,可能导致原有商城瘫痪
秒杀活动只是网站营销的一个附加活动,这个活动具有时间短,并发访问量大的特点,如果和网站原有应用部署在一起,必然会对现有业务造成冲击,稍有不慎可能导致整个网站瘫痪。
解决方案:将秒杀系统独立部署,独立域名。
1.2 带宽问题
假设商品页面大小1M(主要是商品图片大小),那么10000用户并发,需要的网络带宽是:10G(1M×10000),这些网络带宽是因为秒杀活动新增的,超过网站平时使用的带宽。
解决方案:因为秒杀新增的网络带宽,必须和运营商重新购买或者租借。为了减轻网站服务器的压力,需要将秒杀商品页面缓存在CDN,同样需要和CDN服务商临时租借新增的出口带宽。
1.3 有大部分请求不会生成订单
接入层(nginx)漏桶限流。真正进入php和mysql等应用层的流量极少,大多被过滤。
1.4 超卖问题
2.秒杀难点行业主流解决方案
2.1 请求负载大的行业主流解决方法
1 队列
2 负载均衡:更多的机器和PHP应用来接收下单请求
2.2 超卖问题的行业主流解决方法
2.2.1 mysql悲观锁
悲观锁,正如其名,它指的是对数据被外界(包括当前系统的其它事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排它性,否则,即使在本系统中实现了加锁机
制,也无法保证外部系统不会修改数据)。
2.2.2 mysql乐观锁
乐观锁认为一般情况下数据不会造成冲突,所以在数据进行提交更新时才会对数据的冲突与否进行检测。如果没有冲
突那就OK;如果出现冲突了,则返回错误信息并让用户决定如何去做。
乐观锁在数据库上的实现完全是逻辑的,数据库本身不提供支持,而是需要开发者自己来实现。
总结:
乐观锁不锁数据,而是通过版本号控制,会有不同结果返回给php,把决策权交给php。
对比:
乐观锁:不需要锁数据,性能高于悲观锁
2.2.3 PHP+队列
序列化,不会产生多个线程之间的冲突
2.2.4 PHP+redis分布式锁,以及分布式锁主流优化方案
相当于是php线程锁,100000个抢购请求并发过来,有100000个线程,但同一时刻只会有一个线程在执行业务代码,其它线程都在死循环中等待。
分布式锁示例代码:
$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; //下步操作....
}
}
问题:100000个人同时进来这个代码, 始终只有1个人在执行库存等业务操作,其它的都在死循环中等待锁的释放
优化方式:设置更对的锁,比如抢购20个商品,就可以设置20个锁,100000个人进来, 就有20个线程是在执行业务逻辑的,其它的就在等待。
2.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("");
$rob_total = 10; //抢购数量
if($mywatchkey<$rob_total){
$redis->watch("mywatchkey");
$redis->multi(); //设置延迟,方便测试效果。
sleep(5); //插入抢购数据
$redis->hSet("mywatchlist","user_id_".mt_rand(1, 9999),time());
$redis->set("mywatchkey",$mywatchkey+1);
$rob_result = $redis->exec(); if($rob_result){
$mywatchlist = $redis->hGetAll("mywatchlist");
echo "抢购成功!";
echo "剩余数量:".($rob_total-$mywatchkey-1)."";
echo "用户列表:";
var_dump($mywatchlist);
}else{
echo "手气不好,再抢购!";
exit;
}
}
?>
提炼上面的核心代码:
$redis->watch("mywatchkey"); //声明一个乐观锁
$redis->multi(); //redis事务开始
$redis->set("mywatchkey",$mywatchkey+1); //乐观锁的版本号+1
$rob_result = $redis->exec();//redis事务提交
优点如下:
- 首先选用内存数据库来抢购速度极快。
- 速度快并发自然没不是问题。
- 使用悲观锁,会迅速增加系统资源。
- 比队列强的多,队列会使你的内存数据库资源瞬间爆棚。
- 使用乐观锁,达到综合需求。
2.2.5 nginx+lua+redis乐观锁
nginx.conf
user nobody;
worker_processes 1;
events {
worker_connections 1024;
}
http {
#/usr/share/lua/5.1/lua-resty-limit-traffic-master/lib/?.lua;;
lua_package_path "/usr/share/lua/5.1/lua-resty-limit-traffic-master/lib/?.lua;;/usr/share/lua/5.1/lua-resty-redis/lib/?.lua;;/usr/share/lua/5.1/lua-resty-redis-cluster/lib/resty‘7/?.lua;;";
lua_package_cpath "/usr/share/lua/5.1/lua-resty-redis-cluster/lib/libredis_slot.so;;";
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
lua_shared_dict my_limit_req_store 100m;
server {
listen 80;
server_name 127.0.0.1;
server_name 192.168.232.200;
#获取广告推荐数据
location /miao_sha {
default_type 'application/x-javascript;charset=utf-8';
content_by_lua_file /etc/nginx/lua/miao_sha.lua;
}
#下单到服务层+mysql
location /create_order {
allow 127.0.0.1;
#deny all;
#default_type 'application/x-javascript;charset=utf-8';
#rewrite https//www.baidu.com/ break;
#proxy_pass http://192.168.232.201:18306/goods/getList;
content_by_lua '
ngx.header.content_type="text/plain"
ngx.say("从服务层+mysql获取数据")
';
}
}
}
miao_sha.lua
--获取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")
--]]