首頁  >  文章  >  後端開發  >  PHP和Redis實現在高並發下搶購及秒殺

PHP和Redis實現在高並發下搶購及秒殺

小云云
小云云原創
2018-03-13 10:56:593119瀏覽

本文主要和大家分享PHP和Redis實現在高並發下搶購及秒殺,搶購、秒殺是平常很常見的場景,面試的時候面試官也常會問到,比如問你淘寶中的搶購秒殺是怎麼實現的等等,希望能幫助到大家

搶購、秒殺實現很簡單,但是有些問題需要解決,主要針對兩個問題:

一、高並發對資料庫產生的壓力
二、競爭狀態下如何解決庫存的正確減少("超賣"問題)

#第一個問題,對於PHP來說很簡單,用快取技術就可以緩解資料庫壓力,例如memcache,redis等快取技術。
第二個問題就比較複雜點:

常規寫法:
查詢出對應商品的庫存,看是否大於0,然後執行生成訂單等操作,但是在判斷庫存是否大於0處,如果在高並發下就會有問題,導致庫存量出現負數。

<?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(&#39;ymd&#39;).substr(implode(NULL, array_map(&#39;ord&#39;, str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
//记录日志
function insertLog($event,$type=0){
global $conn;
$sql="insert into ih_log(event,type)
values(&#39;$event&#39;,&#39;$type&#39;)";
mysql_query($sql,$conn);
}
//模拟下单操作
//库存是否大于0
$sql="select number from ih_store where goods_id=&#39;$goods_id&#39; and sku_id=&#39;$sku_id&#39;";
//解锁 此时ih_store数据中goods_id=&#39;$goods_id&#39; and sku_id=&#39;$sku_id&#39; 的数据被锁住(注3),其它事务必须等待此次事务 提交后才能执行
$rs=mysql_query($sql,$conn);
$row=mysql_fetch_assoc($rs);
if($row[&#39;number&#39;]>0){//高并发下会导致超卖
$order_sn=build_order_no();
//生成订单
$sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
values(&#39;$order_sn&#39;,&#39;$user_id&#39;,&#39;$goods_id&#39;,&#39;$sku_id&#39;,&#39;$price&#39;)";
$order_rs=mysql_query($sql,$conn);
//库存减少
$sql="update ih_store set number=number-{$number} where sku_id=&#39;$sku_id&#39;";
$store_rs=mysql_query($sql,$conn);
if(mysql_affected_rows()){
insertLog(&#39;库存减少成功&#39;);
}else{
insertLog(&#39;库存减少失败&#39;);
}
}else{
insertLog(&#39;库存不够&#39;);
}
出现这种情况怎么办呢?来看几种优化方法:
优化方案1:将库存字段number字段设为unsigned,当库存为0时,因为字段不能为负数,将会返回false
//库存减少
$sql="update ih_store set number=number-{$number} where sku_id=&#39;$sku_id&#39; and number>0";
$store_rs=mysql_query($sql,$conn);
if(mysql_affected_rows()){
insertLog(&#39;库存减少成功&#39;);
}
优化方案2:使用MySQL的事务,锁住操作的行
<?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(&#39;ymd&#39;).substr(implode(NULL, array_map(&#39;ord&#39;, str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
//记录日志
function insertLog($event,$type=0){
global $conn;
$sql="insert into ih_log(event,type)
values(&#39;$event&#39;,&#39;$type&#39;)";
mysql_query($sql,$conn);
}
//模拟下单操作
//库存是否大于0
mysql_query("BEGIN"); //开始事务
$sql="select number from ih_store where goods_id=&#39;$goods_id&#39; and sku_id=&#39;$sku_id&#39; FOR UPDATE";//此时这条记录被锁住,其它事务必须等待此次事务提交后才能执行
$rs=mysql_query($sql,$conn);
$row=mysql_fetch_assoc($rs);
if($row[&#39;number&#39;]>0){
//生成订单
$order_sn=build_order_no();
$sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
values(&#39;$order_sn&#39;,&#39;$user_id&#39;,&#39;$goods_id&#39;,&#39;$sku_id&#39;,&#39;$price&#39;)";
$order_rs=mysql_query($sql,$conn);
//库存减少
$sql="update ih_store set number=number-{$number} where sku_id=&#39;$sku_id&#39;";
$store_rs=mysql_query($sql,$conn);
if(mysql_affected_rows()){
insertLog(&#39;库存减少成功&#39;);
mysql_query("COMMIT");//事务提交即解锁
}else{
insertLog(&#39;库存减少失败&#39;);
}
}else{
insertLog(&#39;库存不够&#39;);
mysql_query("ROLLBACK");
}
优化方案3:使用非阻塞的文件排他锁
<?php
$conn=mysql_connect("localhost","root","123456");
if(!$conn){
echo "connect failed";
exit;
}
mysql_select_db("big-bak",$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(&#39;ymd&#39;).substr(implode(NULL, array_map(&#39;ord&#39;, str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
//记录日志
function insertLog($event,$type=0){
global $conn;
$sql="insert into ih_log(event,type)
values(&#39;$event&#39;,&#39;$type&#39;)";
mysql_query($sql,$conn);
}
$fp = fopen("lock.txt", "w+");
if(!flock($fp,LOCK_EX | LOCK_NB)){
echo "系统繁忙,请稍后再试";
return;
}
//下单
$sql="select number from ih_store where goods_id=&#39;$goods_id&#39; and sku_id=&#39;$sku_id&#39;";
$rs=mysql_query($sql,$conn);
$row=mysql_fetch_assoc($rs);
if($row[&#39;number&#39;]>0){//库存是否大于0
//模拟下单操作
$order_sn=build_order_no();
$sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
values(&#39;$order_sn&#39;,&#39;$user_id&#39;,&#39;$goods_id&#39;,&#39;$sku_id&#39;,&#39;$price&#39;)";
$order_rs=mysql_query($sql,$conn);
//库存减少
$sql="update ih_store set number=number-{$number} where sku_id=&#39;$sku_id&#39;";
$store_rs=mysql_query($sql,$conn);
if(mysql_affected_rows()){
insertLog(&#39;库存减少成功&#39;);
flock($fp,LOCK_UN);//释放锁
}else{
insertLog(&#39;库存减少失败&#39;);
}
}else{
insertLog(&#39;库存不够&#39;);
}
fclose($fp);
优化方案4:使用redis队列,因为pop操作是原子的,即使有很多用户同时到达,也是依次执行,推荐使用(mysql事务在高并发下性能下降很厉害,文件锁的方式也是)
先将商品库存如队列
<?php
$store=1000;
$redis=new Redis();
$result=$redis->connect(&#39;127.0.0.1&#39;,6379);
$res=$redis->llen(&#39;goods_store&#39;);
echo $res;
$count=$store-$res;
for($i=0;$i<$count;$i++){
$redis->lpush(&#39;goods_store&#39;,1);
}
echo $redis->llen(&#39;goods_store&#39;);
抢购、描述逻辑
<?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(&#39;ymd&#39;).substr(implode(NULL, array_map(&#39;ord&#39;, str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
//记录日志
function insertLog($event,$type=0){
global $conn;
$sql="insert into ih_log(event,type)
values(&#39;$event&#39;,&#39;$type&#39;)";
mysql_query($sql,$conn);
}
//模拟下单操作
//下单前判断redis队列库存量
$redis=new Redis();
$result=$redis->connect(&#39;127.0.0.1&#39;,6379);
$count=$redis->lpop(&#39;goods_store&#39;);
if(!$count){
insertLog(&#39;error:no store redis&#39;);
return;
}
//生成订单
$order_sn=build_order_no();
$sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
values(&#39;$order_sn&#39;,&#39;$user_id&#39;,&#39;$goods_id&#39;,&#39;$sku_id&#39;,&#39;$price&#39;)";
$order_rs=mysql_query($sql,$conn);
//库存减少
$sql="update ih_store set number=number-{$number} where sku_id=&#39;$sku_id&#39;";
$store_rs=mysql_query($sql,$conn);
if(mysql_affected_rows()){
insertLog(&#39;库存减少成功&#39;);
}else{
insertLog(&#39;库存减少失败&#39;);
}

上述只是簡單模擬高並發下的搶購,真實場景要比這複雜很多,很多注意的地方,如搶購頁面做成靜態的,透過ajax呼叫介面。

再如上面的會導致一個用戶搶多個,想法:
需要一個排隊隊列和搶購結果隊列及庫存隊列。高並發情況,先將用戶進入排隊隊列,用一個線程循環處理從排隊隊列取出一個用戶,判斷用戶是否已在搶購結果隊列,如果在,則已搶購,否則未搶購,庫存減1,寫數據庫,將用戶入結果隊​​列。
我之間做商城專案的時候,在秒殺這一塊我直接用的redis,這段時間看了看上面的幾種方法,雖然各有不同,但是實現目的都一樣的,各位自己選擇,開心就好。

相關推薦:

php+redis實作搶購功能

#php如何處理搶購類別功能的高並發請求

PHP透過加鎖實現並發搶購功能

#

以上是PHP和Redis實現在高並發下搶購及秒殺的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn