秒杀系统场景特点
[!TIP|label:正常电子商务流程] 查询商品 — 创建订单 — 扣减库存 — 更新订单 — 付款 — 卖家发货
- 秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。
- 秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。
- 秒杀业务流程比较简单,一般就是下订单减库存。
秒杀架构设计理念
- 限流: 鉴于只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进入服务后端。
- 削峰:对于秒杀系统瞬时会有大量用户涌入,所以在抢购一开始会有很高的瞬间峰值。高峰值流量是压垮系统很重要的原因,所以如何把瞬间的高流量变成一段时间平稳的流量也是设计秒杀系统很重要的思路。实现削峰的常用的方法有利用缓存和消息中间件等技术。
- 异步处理:秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。
- 内存缓存:秒杀系统最大的瓶颈一般都是数据库读写,由于数据库读写属于磁盘IO,性能很低,如果能够把部分数据或业务逻辑转移到内存缓存,效率会有极大地提升。
- 可拓展:当然如果我们想支持更多用户,更大的并发,最好就将系统设计成弹性可拓展的,如果流量来了,拓展机器就好了。像淘宝、京东等双十一活动时会增加大量机器应对交易高峰。
设计思路
[!TIP|label:总结] 前端三板斧【扩容】【限流】【静态化】,后端两条路【内存】+【排队】
前段思路
- 页面静态化:将活动页面上的所有可以静态的元素全部静态化,并尽量减少动态元素。通过CDN来抗峰值。
- 禁止重复提交:用户提交之后按钮置灰,禁止重复提交
- 用户限流:在某一时间段内只允许用户提交一次请求,比如可以采取IP限流
控制层(网关层)
- 限制uid(UserID)访问频率:我们上面拦截了浏览器访问的请求,但针对某些恶意攻击或其它插件,在服务端控制层需要针对同一个访问uid,限制访问频率。
服务层
- 业务分离:将秒杀业务系统和其他业务分离,单独放在高配服务器上,可以集中资源对访问请求抗压。
- 采用消息队列缓存请求:将大流量请求写到消息队列缓存,利用服务器根据自己的处理能力主动到消息缓存队列中抓取任务处理请求,数据库层订阅消息减库存,减库存成功的请求返回秒杀成功,失败的返回秒杀结束。
- 利用缓存应对读请求:对于读多写少业务,大部分请求是查询请求,所以可以读写分离,利用缓存分担数据库压力。
- 利用缓存应对写请求:缓存也是可以应对写请求的,可把数据库中的库存数据转移到Redis缓存中,所有减库存操作都在Redis中进行,然后再通过后台进程把Redis中的用户秒杀请求同步到数据库中。
数据库层
- 数据库层是最脆弱的一层,一般在应用设计时在上游就需要把请求拦截掉,数据库层只承担“能力范围内”的访问请求。所以,上面通过在服务层引入队列和缓存,让最底层的数据库高枕无忧。
基于Redis秒杀&超卖问题解决
- Redis事物
$redis = new redis();
$res = $redis->connect('127.0.0.1', 6379);
if (!$res) {
die('connect error!');
}
$redis->select(6);
//第一次取库存,先用保存到缓存中
$goods_id = 1;
$stock_key = 'goods_id_stock_' . $goods_id;
//先把库存取出来
$stock = $redis->get($stock_key);
//监控key
$redis->watch($stock_key);
//开启事务
$redis->multi();
if ($stock === false) {
echo 'set goodslist';
$info = $db->table('Goods')->get(1);
$stock = $info['stock'];
// 这个地方一定要用setnx,防止一开始并发的时候重复设置
$redis->setnx($stock_key, $info['stock']);
}
if ($stock > 0) {
//加一些延时
sleep(1);
$redis->decr($stock_key);
//把这个产品信息添加到这个用户的订单中
//....一些逻辑代码
//提交事务
$res = $redis->exec();
if ($res === false) {
//实际业务中上面如果有其它对数据库进行的操作这里要使用事务回滚
echo 'unknow';
} else {
echo 'success';
}
} else {
echo 'fail';
}
- Redis队列
//1. 先将商品库存 存入队列
$redis = new Redis();
for($i=1;$i<=100;$i++){
$redis->lpush('good','good_id'.$i);
}
print_r($redis->lrange('good',0,-1));exit;
//2. 队列程序执行
header("content-type:text/html;charset=utf-8");
$redis = new Redis();
//插入抢购数据
$userid = "user_id_".mt_rand(1, 9999).'_'.microtime(true);
if($res = $redis->lpop('good')){
//$left = $redis->llen('good'); //剩余".($left)."
$redis->lpush('good_res',$res);
//file_put_contents('F:\b.txt',$userid."抢购成功!".$res."\n",FILE_APPEND); 写入文件可能会遇到并发锁 导致无法及时写入 而被直接跳过导致记录结果有误 建议测试使用mysql 或者 redis 存入日志记录
}else{
//file_put_contents('F:\b.txt', $userid."手气不好,再抢购!\n",FILE_APPEND);
}
exit;
//3. 打印执行结果
$redis = new Redis();
print_r($redis->lrange('good_res',0,-1));exit;
其他问题
- 对现有网站业务造成冲击
将秒杀系统独立部署,甚至使用独立域名,使其与网站完全隔离。
- 减库存的操作
有两种选择,一种是拍下减库存 另外一种是付款减库存;目前采用的“拍下减库存”的方式,拍下就是一瞬间的事,对用户体验会好些。
- 秒杀之后的支付完成,以及未支付取消占位,如何对剩余库存做及时的控制更新?
数据库里一个状态,未支付。如果超过时间,例如45分钟,库存会重新会恢复(大家熟知的“回仓”),给我们抢票的启示是,开动秒杀后,45分钟之后再试试看,说不定又有票哟~
- 减库存是在那个阶段减呢?如果是下单锁库存的话,大量恶意用户下单锁库存而不支付如何处理呢?
数据库层面写请求量很低,还好,下单不支付,等时间过完再“回仓”
- 刷单处理
- 刷单的情况如下,写一个脚本不间断的发送请求,抢购页面的每次请求用这个用户的标识为一个redis缓存标记加一个过期时间,比如10秒内重复的请求不做处理,起到限流的作用
- 黄牛多买的情况,引导用户到付款成功时才根据活动规则减库存,限制购买个数
- 如果遇到全国大量黄牛集体刷单,比如淘宝双11,小米抢购,在优化购买流程已经到极限的情况下,可以加硬件配置,使用redis分布式架构配合nginx反向代理等方法进行分流引导
其他方案1
优点与说明
- 实现相对简单,能做一部分秒杀活动。
- 前端静态资源走CDN
- 前端做秒杀前后拦截,秒杀中,每个用户只放一次请求到后端。可以拦截掉小白用户的99%的无效点击。
- 利用redis的(setnx、incrby/decrby等原子操作)、mysql的(for update 写锁)实现秒杀库存控制
缺点
- 比较倚重redis
- 过滤用户秒杀条件成为瓶颈
- 业务耦合还是比较严重
其他方案2
优点与说明
- 增加单机本地缓存,可以支持机器平行扩容,抗住更多请求
- 增加mq,做异步处理,解耦复杂的业务逻辑
缺点
- 没有机器人爬取。多个商品秒杀咋办?
- 有没有什么降级处理?
- 数据层还是单点