#事先说明,本次的文章所贴的事例代码并非本人,具体出自什么地方?我也无从考究。不过今天要为大家讲的就是基于这些事例代码结合对应的个人理解进行分析。如果有什么觉得说得不正确的请各位看官拍砖。也让我学而知不足。

#关于秒杀抢购的思路一般都基于三个部分进行设计

1.用户页面层,这个部分可以设置页面缓存,cdn加速,适当的请求拦截。当然前两者相信各位很容易理解,那什么是请求拦截了?其实说白了就是当用户点击了提交按钮后,记得通过ajax把按钮设置为禁用状态。须知道用户在烦躁的时候可是会疯狂地点击提交按钮,这部分的请求如果你不过滤到那岂不是在白白浪费服务器的资源?

2.数据接入层,在数据接入层的这个层面来说我们一般我们就要对用户的请求进行判断,尽量把恶意的请求都拒绝在外,常见的做法就是同一个IP在限定的时间段内限制访问次数,或者通过记录用户的UID来限制用一个用户的UID在每分钟的请求次数,用来过滤一些高端用户通过脚本来参与请求的。

3.数据处理层,最后我们本次文章就是要基于数据处理层的代码展示来为大家说一下关于抢购的处理思路。其实对于抢购和秒杀的核心处理思路就是防止超卖,还有防止服务器迅时流量的爆增导致服务的崩溃。

那么我们先看一个传统的抢购流程

上面这个例子,假设某个抢购场景中,我们一共只有100个商品,在最后一刻,我们已经消耗了99个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是99个,然后都通过了这一个余量判断,最终导致超发。在上面的这个图中,就导致了并发用户B也“抢购成功”,多让一个人获得了商品。这种场景,在高并发的情况下非常容易出现。

优化方案1:将库存字段number字段设为unsigned,当库存为0时,因为字段不能为负数,将会返回false

<?php
//优化方案1:将库存字段number字段设为unsigned,当库存为0时,因为字段不能为负数,将会返回false
include('./mysql.php');
$username = 'wang'.rand(0,1000);
//生成唯一订单
function build_order_no(){return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
//记录日志
function insertLog($event,$type=0,$username){global $conn;$sql="insert into ih_log(event,type,usernma)values('$event','$type','$username')";return mysqli_query($conn,$sql);
}
function insertOrder($order_sn,$user_id,$goods_id,$sku_id,$price,$username,$number)
{global $conn;$sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price,username,number)values('$order_sn','$user_id','$goods_id','$sku_id','$price','$username','$number')";return  mysqli_query($conn,$sql);
}
//模拟下单操作
//库存是否大于0
$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' ";
$rs=mysqli_query($conn,$sql);
$row = $rs->fetch_assoc();if($row['number']>0){//高并发下会导致超卖if($row['number']<$number){return insertLog('库存不够',3,$username);}$order_sn=build_order_no();//库存减少$sql="update ih_store set number=number-{$number} where sku_id='$sku_id' and number>0";$store_rs=mysqli_query($conn,$sql);if($store_rs){//生成订单insertOrder($order_sn,$user_id,$goods_id,$sku_id,$price,$username,$number);insertLog('库存减少成功',1,$username);}else{insertLog('库存减少失败',2,$username);}}else{insertLog('库存不够',3,$username);}
?>
复制代码

当然上述的优化还是不够的,接下来我们要进行的另一个优化方式就是往悲观锁去考虑,什么是悲观锁呢?其实就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。

优化方案2:使用MySQL的事务,锁住操作的行

<?php
//优化方案2:使用MySQL的事务,锁住操作的行
include('./mysql.php');
//生成唯一订单号
function build_order_no(){return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
//记录日志
function insertLog($event,$type=0){global $conn;$sql="insert into ih_log(event,type)values('$event','$type')";mysqli_query($conn,$sql);
}
//模拟下单操作
//库存是否大于0
mysqli_query($conn,"BEGIN");  //开始事务
$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' FOR UPDATE";//此时这条记录被锁住,其它事务必须等待此次事务提交后才能执行
$rs=mysqli_query($conn,$sql);
$row=$rs->fetch_assoc();
if($row['number']>0){//生成订单$order_sn=build_order_no();$sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)values('$order_sn','$user_id','$goods_id','$sku_id','$price')";$order_rs=mysqli_query($conn,$sql);//库存减少$sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";$store_rs=mysqli_query($conn,$sql);if($store_rs){echo '库存减少成功';insertLog('库存减少成功');mysqli_query($conn,"COMMIT");//事务提交即解锁}else{echo '库存减少失败';insertLog('库存减少失败');}
}else{echo '库存不够';insertLog('库存不够');mysqli_query($conn,"ROLLBACK");
}
?>
复制代码

虽然上述的方案的确解决了线程安全的问题,但是,别忘记,我们的场景是“高并发”。也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。同时,这种请求会很多,瞬间增大系统的平均响应时间,结果是可用连接数被耗尽,系统陷入异常。

因此我们就可以采用一种非阻塞模式文件锁的方式来解决这个问题。首先在贴代码之前你可能会问什么是非阻塞呢?简单来说说,文件锁可以分为两种模式,一种是阻塞文件锁,另一种是非阻塞文件锁。阻塞文件锁,会当文件被占用的时候,其他用户无法打开文件且一直在等待过程。而非阻塞文件锁呢,文件在被占用时,可以直接返回false给用户,从而节省用户的等待时间。

优化方案3:非阻塞文件排他锁方式

<?php##注意进入队列的操作这里没有//优化方案3:使用非阻塞的文件排他锁
include ('./mysql.php');
//生成唯一订单号
function build_order_no(){return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
//记录日志
function insertLog($event,$type=0){global $conn;$sql="insert into ih_log(event,type)values('$event','$type')";mysqli_query($conn,$sql);
}
$fp = fopen("lock.txt", "w+");
if(!flock($fp,LOCK_EX | LOCK_NB)){echo "系统繁忙,请稍后再试";return;
}
//下单
$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'";
$rs =  mysqli_query($conn,$sql);
$row = $rs->fetch_assoc();
if($row['number']>0){//库存是否大于0//模拟下单操作$order_sn=build_order_no();$sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)values('$order_sn','$user_id','$goods_id','$sku_id','$price')";$order_rs =  mysqli_query($conn,$sql);//库存减少$sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";$store_rs =  mysqli_query($conn,$sql);if($store_rs){echo '库存减少成功';insertLog('库存减少成功');flock($fp,LOCK_UN);//释放锁}else{echo '库存减少失败';insertLog('库存减少失败');}
}else{echo '库存不够';insertLog('库存不够');
}
fclose($fp);?>
复制代码

对于日IP不高或者说并发数不是很大的应用,用一般的文件操作方法完全没有问题。但如果并发高,在我们对通过使用文件锁操作其实是非常消耗性能的。因此我们可以引入新的思路。

4. FIFO队列思路

那好,那么我们稍微修改一下上面的场景,我们直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),当然这里的队列我们要使用我们耳熟能详的redis队列。

优化思路4:通过引入队列的方式

#先将商品库存如队列<?php
$store=1000;
$redis=new Redis();
$result=$redis->connect('127.0.0.1',6379);
$res=$redis->llen('goods_store');
echo $res;
$count=$store-$res;
for($i=0;$i<$count;$i++){$redis->lpush('goods_store',1);
}
echo $redis->llen('goods_store');
复制代码
#数据处理
<?php
$conn=mysql_connect("localhost","big","123456");
if(!$conn){  echo "connect failed";  exit;
}
mysql_select_db("big",$conn);
mysql_query("set names utf8");$price=10;
$user_id=1;
$goods_id=1;
$sku_id=11;
$number=1;//生成唯一订单号
function build_order_no(){return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
//记录日志
function insertLog($event,$type=0){global $conn;$sql="insert into ih_log(event,type) values('$event','$type')";  mysql_query($sql,$conn);
}//模拟下单操作
//下单前判断redis队列库存量
$redis=new Redis();
$result=$redis->connect('127.0.0.1',6379);
$count=$redis->lpop('goods_store');
if(!$count){insertLog('error:no store redis');return;
}//生成订单
$order_sn=build_order_no();
$sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
$order_rs=mysql_query($sql,$conn); //库存减少
$sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
$store_rs=mysql_query($sql,$conn);
if(mysql_affected_rows()){  insertLog('库存减少成功');
}else{  insertLog('库存减少失败');
} 复制代码

那么新的问题来了,高并发的场景下,因为请求很多,很可能一瞬间将队列内存“撑爆”,然后系统又陷入到了异常状态。或者设计一个极大的内存队列,也是一种方案,但是,系统处理完一个队列内请求的速度根本无法和疯狂涌入队列中的数目相比。也就是说,队列内的请求会越积累越多,最终Web系统平均响应时候还是会大幅下降,系统还是陷入异常。

这个时候,我们就可以讨论一下“乐观锁”的思路了。乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。实现就是,这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。这样的话,我们就不需要考虑队列的问题,不过,它会增大CPU的计算开销。但是,综合来说,这是一个比较好的解决方案。

有很多软件和服务都“乐观锁”功能的支持,例如Redis中的watch就是其中之一。通过这个实现,我们保证了数据的安全。

<?php
$redis = new redis();$result = $redis->connect('127.0.0.1', 6379);echo $mywatchkey = $redis->get("mywatchkey");$rob_total = 100;   //抢购数量
if($mywatchkey<=$rob_total){$redis->watch("mywatchkey");$redis->multi(); //在当前连接上启动一个新的事务。//插入抢购数据$redis->set("mywatchkey",$mywatchkey+1);$rob_result = $redis->exec();if($rob_result){$redis->hSet("watchkeylist","user_".mt_rand(1, 9999),$mywatchkey);$mywatchlist = $redis->hGetAll("watchkeylist");echo "抢购成功!<br/>";echo "剩余数量:".($rob_total-$mywatchkey-1)."<br/>";echo "用户列表:<pre>";var_dump($mywatchlist);}else{$redis->hSet("watchkeylist","user_".mt_rand(1, 9999),'meiqiangdao');echo "手气不好,再抢购!";exit;}
}
?>#注意请购成功的用户,需要另外写定时任务去处理成功的用户,这里的mt_rand演示生成用户名复制代码

#到此,关于抢购秒杀的应用优化思路暂时告一段落。如果上述理解有误请各位留言提供你们的思路,或者你们认为更好的方法让我学习下。谢谢

关于抢购秒杀的实现思路与事例代码相关推荐

  1. java redis实现抢购_【抢购/秒杀】redis实现高并发下的抢购/秒杀功能

    问题: 抢购/秒杀是如今很常见的一个应用场景,那么高并发竞争下如何解决超抢(或超卖库存不足为负数的问题)呢? 常规写法: 查询出对应商品的库存,看是否大于0,然后执行生成订单等操作,但是在判断库存是否 ...

  2. 电商抢购秒杀系统的设计_1_应用场景分析

    2019独角兽企业重金招聘Python工程师标准>>> 电商抢购秒杀系统的设计_1_应用场景分析 概述 所谓知已知彼,百战不殆,在开始详细介绍实战中的抢购秒杀系统时,我们了解一些抢购 ...

  3. php 锁的使用场景,抢购秒杀的场景使用锁个人认为不太合理?

    在抢购秒杀的构架设计中,网上很多都说为了防止超卖现象,应该使用锁机制来做,只有拿到锁的用户才能抢购下单;但是我觉得这个不太合理,在高并发下使用锁,一来造成请求阻塞,二来会造成抢购的不公平现象. 所以我 ...

  4. php抢购排队是怎样做的,基于swoole的抢购排队通用中间件,适合抢购秒杀场景,跟具体业务解耦...

    queue Note: Replace kcloze kcloze https://github.com/YaochufaTech/swoole-queue pei.greet@gmail.com y ...

  5. 简单实现redis实现高并发下的抢购/秒杀功能

    简述 抢购/秒杀是如今很常见的一个应用场景,那么高并发竞争下如何解决超抢(或超卖库存不足为负数的问题)呢? 常规写法: 查询出对应商品的库存,看是否大于0,然后执行生成订单等操作,但是在判断库存是否大 ...

  6. java抢购_java redis 实现抢购秒杀

    2018.10.24 今天研究了下抢购秒杀的功能实现 网上查了一大堆 用redis的最多. 主要是通过redis的 watch multi 事务来控制秒杀数量 不超卖. 这里说下自己的感受: 不超卖的 ...

  7. php redis下单,redis 队列简单实现高并发抢购/秒杀

    redis 队列简单实现高并发抢购/秒杀 2019-03-21 14:34 阅读数 82 前提为每人限购1件 <>开抢前 把秒杀商品库存存进 Redis 队列中 $redis = new ...

  8. 《大厂算法面试题目与答案汇总,剑指offer等常考算法题思路,python代码》V1.0版...

    为了进入大厂,我想很多人都会去牛客.知乎.CSDN等平台去查看面经,了解各个大厂在问技术问题的时候都会问些什么样的问题. 在看了几十上百篇面经之后,我将算法工程师的各种类型最常问到的问题都整理了出来, ...

  9. ML之LGBMRegressor(Competition):2018年全国大学生计算机技能应用大赛《住房月租金预测大数据赛》——设计思路以及核心代码—191017再次更新

    ML之LGBMRegressor(Competition):2018年全国大学生计算机技能应用大赛<住房月租金预测大数据赛>--设计思路以及核心代码-191017再次更新 目录 竞赛相关信 ...

  10. 剑指OFFER思路总结与代码分享——树篇(Java实现)

    剑指OFFER树相关 55-1 二叉树的深度 27 二叉树的镜像 54 二叉搜索树的第K大节点 32-II 从上到下打印二叉树 07 重建二叉树 68-I 二叉搜索树的最近公共祖先 68-II 二叉树 ...

最新文章

  1. 火力发电厂与变电站设计防火规范_建筑内部装修设计防火规范-GB 50222-2017
  2. MyBatis对于Java对象里的枚举类型处理
  3. linux安全pdf,linux系统安全加固.pdf
  4. 深度学习目标检测系列:一文弄懂YOLO算法|附Python源码
  5. u盘序列号读取工具_硬盘读写工具
  6. linux传输文件命令scp,linux文件传输命令:SCP用法
  7. 英语总结系列(二十九):好好学英语
  8. 1319. 连通网络的操作次数
  9. python是什么 自学-你是如何自学 Python 的?
  10. 财联社24小时电报关键词监控提醒
  11. java反应器构型_27种反应器的结构及原理,你想了解的都在这里
  12. PS2022安装步骤 ps 2022(详细安装方法)
  13. 计算机内存die,从内存时序的角度告诉你 三星B-DIE为何成为高端所用
  14. 2018 Google IO大会来了
  15. 在Python中用WordCloud生成聊天记录热点词汇词云图
  16. Oracle Linux网卡参数默认设置导致ORA-603
  17. 计算机网络的类型和特点
  18. linux下,matplotlib遇到的相关问题以及解决方法
  19. 基于vspd DLL二次开发的虚拟串口工具
  20. 创意编程——随机(扩散限制聚集DLA)

热门文章

  1. 联想win7无法连接无线网络连接服务器,联想笔记本连不上wifi该怎么处理
  2. 45个免费LOGO在线制作网站
  3. 中台详解(上)-什么是中台
  4. 使用Flink Metric Reporter 对flink任务指标进行监控
  5. Publish Over SSH 本地安装
  6. Ubuntu 中文字体美化方案大全 (3): 使用Windows XP字体
  7. JSD-2204-(业务逻辑开发)-发酷鲨商城front模块-开发购物车功能-Day09
  8. 3D变形:平移、旋转、缩放
  9. IntelliJ IDEA 整理代码格式 快捷键
  10. django按日期查询数据