日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

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

發(fā)布時(shí)間:2023/12/20 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 关于抢购秒杀的实现思路与事例代码 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

#事先說(shuō)明,本次的文章所貼的事例代碼并非本人,具體出自什么地方?我也無(wú)從考究。不過(guò)今天要為大家講的就是基于這些事例代碼結(jié)合對(duì)應(yīng)的個(gè)人理解進(jìn)行分析。如果有什么覺得說(shuō)得不正確的請(qǐng)各位看官拍磚。也讓我學(xué)而知不足。


#關(guān)于秒殺搶購(gòu)的思路一般都基于三個(gè)部分進(jìn)行設(shè)計(jì)

1.用戶頁(yè)面層,這個(gè)部分可以設(shè)置頁(yè)面緩存,cdn加速,適當(dāng)?shù)恼?qǐng)求攔截。當(dāng)然前兩者相信各位很容易理解,那什么是請(qǐng)求攔截了?其實(shí)說(shuō)白了就是當(dāng)用戶點(diǎn)擊了提交按鈕后,記得通過(guò)ajax把按鈕設(shè)置為禁用狀態(tài)。須知道用戶在煩躁的時(shí)候可是會(huì)瘋狂地點(diǎn)擊提交按鈕,這部分的請(qǐng)求如果你不過(guò)濾到那豈不是在白白浪費(fèi)服務(wù)器的資源?

2.數(shù)據(jù)接入層,在數(shù)據(jù)接入層的這個(gè)層面來(lái)說(shuō)我們一般我們就要對(duì)用戶的請(qǐng)求進(jìn)行判斷,盡量把惡意的請(qǐng)求都拒絕在外,常見的做法就是同一個(gè)IP在限定的時(shí)間段內(nèi)限制訪問(wèn)次數(shù),或者通過(guò)記錄用戶的UID來(lái)限制用一個(gè)用戶的UID在每分鐘的請(qǐng)求次數(shù),用來(lái)過(guò)濾一些高端用戶通過(guò)腳本來(lái)參與請(qǐng)求的。

3.數(shù)據(jù)處理層,最后我們本次文章就是要基于數(shù)據(jù)處理層的代碼展示來(lái)為大家說(shuō)一下關(guān)于搶購(gòu)的處理思路。其實(shí)對(duì)于搶購(gòu)和秒殺的核心處理思路就是防止超賣,還有防止服務(wù)器迅時(shí)流量的爆增導(dǎo)致服務(wù)的崩潰。

那么我們先看一個(gè)傳統(tǒng)的搶購(gòu)流程


上面這個(gè)例子,假設(shè)某個(gè)搶購(gòu)場(chǎng)景中,我們一共只有100個(gè)商品,在最后一刻,我們已經(jīng)消耗了99個(gè)商品,僅剩最后一個(gè)。這個(gè)時(shí)候,系統(tǒng)發(fā)來(lái)多個(gè)并發(fā)請(qǐng)求,這批請(qǐng)求讀取到的商品余量都是99個(gè),然后都通過(guò)了這一個(gè)余量判斷,最終導(dǎo)致超發(fā)。在上面的這個(gè)圖中,就導(dǎo)致了并發(fā)用戶B也“搶購(gòu)成功”,多讓一個(gè)人獲得了商品。這種場(chǎng)景,在高并發(fā)的情況下非常容易出現(xiàn)。

優(yōu)化方案1:將庫(kù)存字段number字段設(shè)為unsigned,當(dāng)庫(kù)存為0時(shí),因?yàn)樽侄尾荒転樨?fù)數(shù),將會(huì)返回false

<?php //優(yōu)化方案1:將庫(kù)存字段number字段設(shè)為unsigned,當(dāng)庫(kù)存為0時(shí),因?yàn)樽侄尾荒転樨?fù)數(shù),將會(huì)返回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); } //模擬下單操作 //庫(kù)存是否大于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){//高并發(fā)下會(huì)導(dǎo)致超賣if($row['number']<$number){return insertLog('庫(kù)存不夠',3,$username);}$order_sn=build_order_no();//庫(kù)存減少$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('庫(kù)存減少成功',1,$username);}else{insertLog('庫(kù)存減少失敗',2,$username);}}else{insertLog('庫(kù)存不夠',3,$username);} ?> 復(fù)制代碼

當(dāng)然上述的優(yōu)化還是不夠的,接下來(lái)我們要進(jìn)行的另一個(gè)優(yōu)化方式就是往悲觀鎖去考慮,什么是悲觀鎖呢?其實(shí)就是在修改數(shù)據(jù)的時(shí)候,采用鎖定狀態(tài),排斥外部請(qǐng)求的修改。遇到加鎖的狀態(tài),就必須等待。


優(yōu)化方案2:使用MySQL的事務(wù),鎖住操作的行

<?php //優(yōu)化方案2:使用MySQL的事務(wù),鎖住操作的行 include('./mysql.php'); //生成唯一訂單號(hào) 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); } //模擬下單操作 //庫(kù)存是否大于0 mysqli_query($conn,"BEGIN"); //開始事務(wù) $sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' FOR UPDATE";//此時(shí)這條記錄被鎖住,其它事務(wù)必須等待此次事務(wù)提交后才能執(zhí)行 $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);//庫(kù)存減少$sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";$store_rs=mysqli_query($conn,$sql);if($store_rs){echo '庫(kù)存減少成功';insertLog('庫(kù)存減少成功');mysqli_query($conn,"COMMIT");//事務(wù)提交即解鎖}else{echo '庫(kù)存減少失敗';insertLog('庫(kù)存減少失敗');} }else{echo '庫(kù)存不夠';insertLog('庫(kù)存不夠');mysqli_query($conn,"ROLLBACK"); } ?> 復(fù)制代碼

雖然上述的方案的確解決了線程安全的問(wèn)題,但是,別忘記,我們的場(chǎng)景是“高并發(fā)”。也就是說(shuō),會(huì)很多這樣的修改請(qǐng)求,每個(gè)請(qǐng)求都需要等待“鎖”,某些線程可能永遠(yuǎn)都沒有機(jī)會(huì)搶到這個(gè)“鎖”,這種請(qǐng)求就會(huì)死在那里。同時(shí),這種請(qǐng)求會(huì)很多,瞬間增大系統(tǒng)的平均響應(yīng)時(shí)間,結(jié)果是可用連接數(shù)被耗盡,系統(tǒng)陷入異常。

因此我們就可以采用一種非阻塞模式文件鎖的方式來(lái)解決這個(gè)問(wèn)題。首先在貼代碼之前你可能會(huì)問(wèn)什么是非阻塞呢?簡(jiǎn)單來(lái)說(shuō)說(shuō),文件鎖可以分為兩種模式,一種是阻塞文件鎖,另一種是非阻塞文件鎖。阻塞文件鎖,會(huì)當(dāng)文件被占用的時(shí)候,其他用戶無(wú)法打開文件且一直在等待過(guò)程。而非阻塞文件鎖呢,文件在被占用時(shí),可以直接返回false給用戶,從而節(jié)省用戶的等待時(shí)間。

優(yōu)化方案3:非阻塞文件排他鎖方式

<?php##注意進(jìn)入隊(duì)列的操作這里沒有//優(yōu)化方案3:使用非阻塞的文件排他鎖 include ('./mysql.php'); //生成唯一訂單號(hào) 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 "系統(tǒng)繁忙,請(qǐng)稍后再試";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){//庫(kù)存是否大于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);//庫(kù)存減少$sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";$store_rs = mysqli_query($conn,$sql);if($store_rs){echo '庫(kù)存減少成功';insertLog('庫(kù)存減少成功');flock($fp,LOCK_UN);//釋放鎖}else{echo '庫(kù)存減少失敗';insertLog('庫(kù)存減少失敗');} }else{echo '庫(kù)存不夠';insertLog('庫(kù)存不夠'); } fclose($fp);?> 復(fù)制代碼

對(duì)于日IP不高或者說(shuō)并發(fā)數(shù)不是很大的應(yīng)用,用一般的文件操作方法完全沒有問(wèn)題。但如果并發(fā)高,在我們對(duì)通過(guò)使用文件鎖操作其實(shí)是非常消耗性能的。因此我們可以引入新的思路。

4. FIFO隊(duì)列思路

那好,那么我們稍微修改一下上面的場(chǎng)景,我們直接將請(qǐng)求放入隊(duì)列中的,采用FIFO(First Input First Output,先進(jìn)先出),當(dāng)然這里的隊(duì)列我們要使用我們耳熟能詳?shù)膔edis隊(duì)列。

優(yōu)化思路4:通過(guò)引入隊(duì)列的方式

#先將商品庫(kù)存如隊(duì)列<?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'); 復(fù)制代碼

#數(shù)據(jù)處理 <?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;//生成唯一訂單號(hào) 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隊(duì)列庫(kù)存量 $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); //庫(kù)存減少 $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'"; $store_rs=mysql_query($sql,$conn); if(mysql_affected_rows()){ insertLog('庫(kù)存減少成功'); }else{ insertLog('庫(kù)存減少失敗'); } 復(fù)制代碼

那么新的問(wèn)題來(lái)了,高并發(fā)的場(chǎng)景下,因?yàn)檎?qǐng)求很多,很可能一瞬間將隊(duì)列內(nèi)存“撐爆”,然后系統(tǒng)又陷入到了異常狀態(tài)。或者設(shè)計(jì)一個(gè)極大的內(nèi)存隊(duì)列,也是一種方案,但是,系統(tǒng)處理完一個(gè)隊(duì)列內(nèi)請(qǐng)求的速度根本無(wú)法和瘋狂涌入隊(duì)列中的數(shù)目相比。也就是說(shuō),隊(duì)列內(nèi)的請(qǐng)求會(huì)越積累越多,最終Web系統(tǒng)平均響應(yīng)時(shí)候還是會(huì)大幅下降,系統(tǒng)還是陷入異常。

這個(gè)時(shí)候,我們就可以討論一下“樂(lè)觀鎖”的思路了。樂(lè)觀鎖,是相對(duì)于“悲觀鎖”采用更為寬松的加鎖機(jī)制,大都是采用帶版本號(hào)(Version)更新。實(shí)現(xiàn)就是,這個(gè)數(shù)據(jù)所有請(qǐng)求都有資格去修改,但會(huì)獲得一個(gè)該數(shù)據(jù)的版本號(hào),只有版本號(hào)符合的才能更新成功,其他的返回?fù)屬?gòu)失敗。這樣的話,我們就不需要考慮隊(duì)列的問(wèn)題,不過(guò),它會(huì)增大CPU的計(jì)算開銷。但是,綜合來(lái)說(shuō),這是一個(gè)比較好的解決方案。


有很多軟件和服務(wù)都“樂(lè)觀鎖”功能的支持,例如Redis中的watch就是其中之一。通過(guò)這個(gè)實(shí)現(xiàn),我們保證了數(shù)據(jù)的安全。

<?php $redis = new redis();$result = $redis->connect('127.0.0.1', 6379);echo $mywatchkey = $redis->get("mywatchkey");$rob_total = 100; //搶購(gòu)數(shù)量 if($mywatchkey<=$rob_total){$redis->watch("mywatchkey");$redis->multi(); //在當(dāng)前連接上啟動(dòng)一個(gè)新的事務(wù)。//插入搶購(gòu)數(shù)據(jù)$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 "搶購(gòu)成功!<br/>";echo "剩余數(shù)量:".($rob_total-$mywatchkey-1)."<br/>";echo "用戶列表:<pre>";var_dump($mywatchlist);}else{$redis->hSet("watchkeylist","user_".mt_rand(1, 9999),'meiqiangdao');echo "手氣不好,再搶購(gòu)!";exit;} } ?>#注意請(qǐng)購(gòu)成功的用戶,需要另外寫定時(shí)任務(wù)去處理成功的用戶,這里的mt_rand演示生成用戶名復(fù)制代碼

#到此,關(guān)于搶購(gòu)秒殺的應(yīng)用優(yōu)化思路暫時(shí)告一段落。如果上述理解有誤請(qǐng)各位留言提供你們的思路,或者你們認(rèn)為更好的方法讓我學(xué)習(xí)下。謝謝


總結(jié)

以上是生活随笔為你收集整理的关于抢购秒杀的实现思路与事例代码的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。