php生成 sku_高并发下,php与redis实现的抢购、秒杀功能
搶購、秒殺是如今很常見的一個應(yīng)用場景,主要需要解決的問題有兩個:
1 高并發(fā)對數(shù)據(jù)庫產(chǎn)生的壓力
2 競爭狀態(tài)下如何解決庫存的正確減少("超賣"問題)
對于第一個問題,已經(jīng)很容易想到用緩存來處理搶購,避免直接操作數(shù)據(jù)庫,例如使用Redis。
重點在于第二個問題
常規(guī)寫法:
查詢出對應(yīng)商品的庫存,看是否大于0,然后執(zhí)行生成訂單等操作,但是在判斷庫存是否大于0處,如果在高并發(fā)下就會有問題,導(dǎo)致庫存量出現(xiàn)負數(shù)
<?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); }//模擬下單操作 //庫存是否大于0 $sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'";//解鎖 此時ih_store數(shù)據(jù)中g(shù)oods_id='$goods_id' and sku_id='$sku_id' 的數(shù)據(jù)被鎖住(注3),其它事務(wù)必須等待此次事務(wù) 提交后才能執(zhí)行 $rs=mysql_query($sql,$conn); $row=mysql_fetch_assoc($rs); if($row['number']>0){//高并發(fā)下會導(dǎo)致超賣$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('庫存減少失敗');} }else{insertLog('庫存不夠'); } ?>優(yōu)化方案1:將庫存字段number字段設(shè)為unsigned,當(dāng)庫存為0時,因為字段不能為負數(shù),將會返回false
//庫存減少 $sql="update ih_store set number=number-{$number} where sku_id='$sku_id' and number>0"; $store_rs=mysql_query($sql,$conn); if(mysql_affected_rows()){ insertLog('庫存減少成功'); }優(yōu)化方案2:使用MySQL的事務(wù),鎖住操作的行
<?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); }//模擬下單操作 //庫存是否大于0 mysql_query("BEGIN"); //開始事務(wù) $sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' FOR UPDATE";//此時這條記錄被鎖住,其它事務(wù)必須等待此次事務(wù)提交后才能執(zhí)行 $rs=mysql_query($sql,$conn); $row=mysql_fetch_assoc($rs); 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=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('庫存減少成功');mysql_query("COMMIT");//事務(wù)提交即解鎖}else{ insertLog('庫存減少失敗');} }else{insertLog('庫存不夠');mysql_query("ROLLBACK"); } ?>優(yōu)化方案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('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); }$fp = fopen("lock.txt", "w+"); if(!flock($fp,LOCK_EX | LOCK_NB)){echo "系統(tǒng)繁忙,請稍后再試";return; } //下單 $sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'"; $rs=mysql_query($sql,$conn); $row=mysql_fetch_assoc($rs); 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=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('庫存減少成功');flock($fp,LOCK_UN);//釋放鎖}else{ insertLog('庫存減少失敗');} }else{insertLog('庫存不夠'); } fclose($fp);優(yōu)化方案4:使用redis隊列,因為pop操作是原子的,即使有很多用戶同時到達,也是依次執(zhí)行,推薦使用(mysql事務(wù)在高并發(fā)下性能下降很厲害,文件鎖的方式也是)
先將商品庫存如隊列
<?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('庫存減少失敗'); }模擬5000高并發(fā)測試
webbench -c 5000 -t 60 http://192.168.1.198/big/index.php
ab -r -n 6000 -c 5000 http://192.168.1.198/big/index.php
上述只是簡單模擬高并發(fā)下的搶購,真實場景要比這復(fù)雜很多,很多注意的地方
如搶購頁面做成靜態(tài)的,通過ajax調(diào)用接口
再如上面的會導(dǎo)致一個用戶搶多個,思路:
需要一個排隊隊列和搶購結(jié)果隊列及庫存隊列。高并發(fā)情況,先將用戶進入排隊隊列,用一個線程循環(huán)處理從排隊隊列取出一個用戶,判斷用戶是否已在搶購結(jié)果隊列,如果在,則已搶購,否則未搶購,庫存減1,寫數(shù)據(jù)庫,將用戶入結(jié)果隊列。
測試數(shù)據(jù)表
-- -- 數(shù)據(jù)庫: `big` ---- ---------------------------------------------------------- -- 表的結(jié)構(gòu) `ih_goods` --CREATE TABLE IF NOT EXISTS `ih_goods` (`goods_id` int(10) unsigned NOT NULL AUTO_INCREMENT,`cat_id` int(11) NOT NULL,`goods_name` varchar(255) NOT NULL,PRIMARY KEY (`goods_id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=2 ;-- -- 轉(zhuǎn)存表中的數(shù)據(jù) `ih_goods` --INSERT INTO `ih_goods` (`goods_id`, `cat_id`, `goods_name`) VALUES (1, 0, '小米手機');-- ---------------------------------------------------------- -- 表的結(jié)構(gòu) `ih_log` --CREATE TABLE IF NOT EXISTS `ih_log` (`id` int(11) NOT NULL AUTO_INCREMENT,`event` varchar(255) NOT NULL,`type` tinyint(4) NOT NULL DEFAULT '0',`addtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;-- -- 轉(zhuǎn)存表中的數(shù)據(jù) `ih_log` ---- ---------------------------------------------------------- -- 表的結(jié)構(gòu) `ih_order` --CREATE TABLE IF NOT EXISTS `ih_order` (`id` int(11) NOT NULL AUTO_INCREMENT,`order_sn` char(32) NOT NULL,`user_id` int(11) NOT NULL,`status` int(11) NOT NULL DEFAULT '0',`goods_id` int(11) NOT NULL DEFAULT '0',`sku_id` int(11) NOT NULL DEFAULT '0',`price` float NOT NULL,`addtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='訂單表' AUTO_INCREMENT=1 ;-- -- 轉(zhuǎn)存表中的數(shù)據(jù) `ih_order` ---- ---------------------------------------------------------- -- 表的結(jié)構(gòu) `ih_store` --CREATE TABLE IF NOT EXISTS `ih_store` (`id` int(11) NOT NULL AUTO_INCREMENT,`goods_id` int(11) NOT NULL,`sku_id` int(10) unsigned NOT NULL DEFAULT '0',`number` int(10) NOT NULL DEFAULT '0',`freez` int(11) NOT NULL DEFAULT '0' COMMENT '虛擬庫存',PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='庫存' AUTO_INCREMENT=2 ;-- -- 轉(zhuǎn)存表中的數(shù)據(jù) `ih_store` --INSERT INTO `ih_store` (`id`, `goods_id`, `sku_id`, `number`, `freez`) VALUES (1, 1, 11, 500, 0); 以上內(nèi)容希望幫助到大家,很多PHPer在進階的時候總會遇到一些問題和瓶頸,業(yè)務(wù)代碼寫多了沒有方向感,不知道該從那里入手去提升,對此我整理了一些資料,包括但不限于:分布式架構(gòu)、高可擴展、高性能、高并發(fā)、服務(wù)器性能調(diào)優(yōu)、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql優(yōu)化、shell腳本、Docker、微服務(wù)、Nginx等多個知識點高級進階干貨需要的可以免費分享給大家,需要請戳這里鏈接 或 者關(guān)注咱們下面的知乎專欄PHP架構(gòu)師圈子?zhuanlan.zhihu.com總結(jié)
以上是生活随笔為你收集整理的php生成 sku_高并发下,php与redis实现的抢购、秒杀功能的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 新款大众 ID.3 Pro / Pro
- 下一篇: 比亚迪发布半年报:营收2601.2亿 研