秒杀
1、应对并发访问量大,部署和前端方案:
- 秒杀系统独立部署,独立域名;
- 秒杀前按钮置灰,不能发送请求
- 产品层面,用户点击“查询”或“购买”后,按钮置灰,禁止用户重复提交请求;
- js层面,限制用户在x秒内只能提交一次请求;
- 前端缓存,当用户一直刷新页面的时候,前端可以到浏览器里面获取缓存数据
2、带宽问题
- 加大宽带
- 图片或静态文件压缩 (原文件压缩或nginx输出时候压缩)
- 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") --]]

浙公网安备 33010602011771号