php結(jié)合redis實現(xiàn)高并發(fā)下的搶購、秒殺功能的實例
搶購、秒殺是如今很常見的一個應用場景,主要需要解決的問題有兩個:
1 高并發(fā)對數(shù)據(jù)庫產(chǎn)生的壓力
2 競爭狀態(tài)下如何解決庫存的正確減少("超賣"問題)
對于第一個問題,已經(jīng)很容易想到用緩存來處理搶購,避免直接操作數(shù)據(jù)庫,例如使用Redis。
重點在于第二個問題
常規(guī)寫法:
查詢出對應商品的庫存,看是否大于0,然后執(zhí)行生成訂單等操作,但是在判斷庫存是否大于0處,如果在高并發(fā)下就會有問題,導致庫存量出現(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ā)下會導致超賣 $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,當庫存為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ā)下的搶購,真實場景要比這復雜很多,很多注意的地方
如搶購頁面做成靜態(tài)的,通過ajax調(dià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);
以上就是小編為大家?guī)淼膒hp結(jié)合redis實現(xiàn)高并發(fā)下的搶購、秒殺功能的實例全部內(nèi)容了,希望大家多多支持腳本之家~
- php操作redis數(shù)據(jù)庫常見方法實例總結(jié)
- PHP操作Redis數(shù)據(jù)庫常用方法示例
- PHP數(shù)據(jù)庫操作三:redis用法分析
- PHP實現(xiàn)的redis主從數(shù)據(jù)庫狀態(tài)檢測功能示例
- PHP的Laravel框架結(jié)合MySQL與Redis數(shù)據(jù)庫的使用部署
- php實現(xiàn)redis數(shù)據(jù)庫指定庫號遷移的方法
- 30個php操作redis常用方法代碼例子
- redis 隊列操作的例子(php)
- php Session存儲到Redis的方法
- PHP操作Redis常用技巧總結(jié)
- php+redis實現(xiàn)商城秒殺功能
- php操作redis緩存方法分享
- php操作Redis數(shù)據(jù)庫基本示例【安裝、連接、設(shè)置、查詢、斷開】
相關(guān)文章

Redis創(chuàng)建并修改Lua 環(huán)境的實現(xiàn)方法

Redis在Ubuntu系統(tǒng)上無法啟動的問題排查