秒杀的特点
1.持续性短 -- 》 短时间内的活动,可能 12:00开始,12:01就结束了
2.速度快 -- 》 程序的处理必须要快
3.访问量大 -- 》会存在大量的用户涌入
4.业务简单 -- 》 基本流程上就是,判断库存->减库存->下订单
5.命中少 -- 》 不能出现超卖,比如100w用户购买抢购,商品库存只有100件,那就只能卖出100件
6.读多写少 -- 》 项目访问基本上都是以读为主要的操作,写相对较少
秒杀要考虑的问题
- 1.关键问题:超卖
秒杀活动一般都是“赔本挣吆喝”,也是多销盈利,本来价格就低,要是出现超卖现象,不就亏本了
- 2.来历不明的恶意请求或者黄牛的疯抢
树大招风、低价格适宜的都是比较吸引人的;因此总有恶意破坏的、暗中当黄牛的很多;混淆在真实的用户群里面难以分辨
- 3.链接没有处理好直接暴露出来
但凡了解程序的都知道f12,就可以看到html页面,url更是无所遁形;写爬虫同学不就可以写一个程序疯狂抢购了嘛
- 4.数据库:太脆弱、容易挂
数据库在上万甚至上十几万的QPS的时候,根本扛不住;特别脆弱像一张纸一样;
- 5.缓存流量集中
运用Redis之后,如果不做好处理也会流量集中在一台机器上;这样就会造成流量集中访问,redis集群的效果就没用
好
秒杀设计时要想的问题
- 前端页面
在秒杀前我们需要先考虑前端的页面:
1.页面静态化:将需要秒杀的商品转为静态化(可以根据页面的实际情况划分不同的唯度存储在缓存中),在秒
杀环节中可提前放入到CDN服务器中
2.按钮控制:注意秒杀前一秒用户会狂点,往死里点,还是物理外挂的点击(没错说的就是你);应对在秒杀开
始前先按钮置灰,在到时间点了也再灰几秒,避免到时间点集中点击(到时间点击停不下来)
3.前端限流:在点击一次按钮之后,需要过几秒钟才能继续点击,不能连续点击
这样做可以将流量砍去一部分,也算是限流的一种方法
- 地址保护
一般做网页的开发者,默认都会选择对外提供,自己项目的url;懂得f12的小伙子们都知道怎么去查url地址,这个时候如果后台的请求被黄牛、黑客发现;那么这是一个非常危险的存在(对于超具有诱惑力的商品来说,当然秒杀商品要是拖鞋,抹布啥的,可能就不用控制了吧。。。)
1.连接保护:可以将url动态化,可基于MD5的方式加密再处理之后的字符串去作为url,前端只需要通过前端代码获取url后台校验才能通过
流量可以砍去一部分,针对不正当手段的
- nginx
1.负载均衡: 在nginx中也可以增加负载均衡,进行分流
2.限流:拦截恶意的流量利用limit的方式可以控制,再可以基于lua结合Redis再进一步的提升限流功能(漏桶限流)
这样真正进入到程序的流量就又变少很多了
- **后端服务 **
针对秒杀我们往往可以选择单独构建出一个秒杀的服务,其中代码可以复用之前正常的业务流程的代
码;
1.职责单一:在秒杀中需针对性的构建、秒杀服务
2.业务隔离:从业务上需要把秒杀和日常的售卖商品进行区分,针对需要进行秒杀的商品,提前申请并根据商品
提前生成静态页面上传到CDN中,将商品库存在活动开始前也预热到Redis中
3.择优选择:在秒杀期间也可以考虑针对一些用户访问并不是很高的功能可以先限流,优先应对好秒杀的业务
4.部署隔离:秒杀相关的服务和日常服务要分组部署,不能因为秒杀出问题影响正常的业务服务
5.高可用:集群,微服务,分布式
- redis
秒杀的本质实际上就是对库存的抢夺;(这要直接操作mysql,等着芭比Q吧)
1.预热:提前将需要秒杀的商品写入到数据库中,在秒杀结束了再异步修改数据库中的库存
2.高可用:
a.主从+哨兵:如果是利用主从的话需要自己在程序中实现好对应的读写分离,从节点的负载均衡
b.集群:如果是集
群我们就需要考虑问题,可能会集中写入访问;建议可以对秒杀的库存进行划分 id_1,id_2,id_3这样的方式划分库存可让iD能够分散到Redis的集群的各个节点上,充分的利用读写的负载均衡
3.事务-锁:Redis本身是支持事务,在用的时候可以考虑是利用管道,乐观锁,分布式锁来避免超卖问题
- 削锋-mq
主要是针对目前流量比较高的时候;可以利用rabbitmq的方式处理
1.队列:任务进来就可以写入到mq中,然后利用进程对mq监听消费
- 数据库
独立:针对秒杀的情况避免秒杀活动影响到日常售卖的业务,Redis缓存需要单独部署,mysql也可以单独配置在
秒杀结束后剩下的库存可以回归到日常库存中,秒杀和日常的订单查询可以在秒杀哦订单后发消息到队列中,
日常订单服务监听记录
- 注意:以上只是说说秒杀要注意的,就是一些大体的思路,如果公司有很大的流量,具体细节还是要和公司的大佬们慢慢的讨论才能得出每个步骤具体是什么样的,本人也只是个弟弟,也没有对每个步骤进行具体的分析和研究,这里只是提供一些注意点和要考虑的问题,不喜勿喷,有问题请提出,大家一起研究,共同进步
秒杀的超卖问题
- 为什么要防止超卖问题,这个就不用说了,大家都懂...
if(库存数量 >= 下单数){
可以购买
购买成功,然后把库存数量减少
}else{
不能购买
}
这个伪代码看着好像没啥问题,但是并发的时候,一定会出现超卖的问题
- 超卖的一些解决办法
1.mysql的悲观锁
2.mysql的乐观锁
3.php/go + 队列
4.php/go + redis分布式锁
5.php/go + redis乐观锁 redis watch
6.nginx + lua + redis watch(乐观锁)
// 5和6是一样的意思,只是6是在nginx里面操作的,就不用在逻辑层去做这个判断,本质都是redis的乐观锁实现
- 1.mysql悲观锁
悲观锁,正如其名,它指的是对数据被外界(包括当前系统的其它事务,以及来自外部系统的事务处理)修改
持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁
机制(也只有数据库层提供的锁机制才能真正保证数据访问的排它性)
mysql的排他锁就是悲观锁
一般做秒杀基本不会使用这种方法是防止超卖。。。。
- 2.mysql乐观锁
乐观锁认为一般情况下数据不会造成冲突,所以在数据进行提交更新时才会对数据的冲突与否进行检测。如果没有冲突那就OK;如果出现冲突了,则返回错误信息并让用户决定如何去做。
乐观锁在数据库上的实现完全是逻辑的,数据库本身不提供支持,而是需要开发者自己来实现。
例子(php)
<?php
// 注意,下面这个只是用来表达意思的代码,不是有效的。
$version = mysqlquery(SELECT VERSION FROM employee)
mysqlquery("UPDATE employee SET money = 1, VERSION=VERSION+1 WHERE VERSION=$version")
?>
总结: 乐观锁不锁数据,而是通过版本号控制,会有不同结果返回给php,把决策权交给php。
- 3.队列
队列的特点:先进先出,排序执行的,序列化,不会产生多个线程之间的冲突
- 4.redis分布式锁
相当于是php线程锁,100000个抢购请求并发过来,有100000个线程,但同一时刻只会有一个线程在执行业务代码,其它线程都在死循环中等待
redis 分布式锁与原理:
redis> EXISTS job # job 不存在
(integer) 0
redis> SETNX job "programmer" # job 设置成功
(integer) 1
redis> SETNX job "code-farmer" # 尝试覆盖 job ,失败
(integer) 0
redis> GET job # 没有被覆盖
"programmer"
可见 SETNX和set是有区别的,SETNX只能1次,set可以无数次的。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;
//下步操作....
}
}
优化方式:设置更对的锁,比如抢购20个商品,就可以设置20个锁, 100000个人进来, 就有20个线程是在执行业务逻辑的,其它的就在等待。
- 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();
//设置延迟,方便测试效果。
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事务提交
该方法的优点:
1.首先选用内存数据库来抢购速度极快
2.速度快并发自然没问题
3.使用悲观锁,会迅速增加系统资源
4.比队列强的多,队列会使你的内存数据库资源瞬间爆棚
5.使用乐观锁,达到综合需求
- 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;
local watch_key= redis_instance:get("sku_num")
ngx.say("goods_num:")
ngx.say(sku_num)
redis_instance:set("sku_num",sku_num);
redis_instance:set("watch_key",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()
以上只是本人自己理解的,如果有问题欢迎指出,大家一起讨论,共同进步