這篇“如何解決PHP高并發(fā)問題”文章的知識(shí)點(diǎn)大部分人都不太理解,所以小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細(xì),步驟清晰,具有一定的借鑒價(jià)值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“如何解決PHP高并發(fā)問題”文章吧。
企業(yè)建站必須是能夠以充分展現(xiàn)企業(yè)形象為主要目的,是企業(yè)文化與產(chǎn)品對(duì)外擴(kuò)展宣傳的重要窗口,一個(gè)合格的網(wǎng)站不僅僅能為公司帶來巨大的互聯(lián)網(wǎng)上的收集和信息發(fā)布平臺(tái),成都創(chuàng)新互聯(lián)公司面向各種領(lǐng)域:白烏魚等成都網(wǎng)站設(shè)計(jì)、營銷型網(wǎng)站建設(shè)解決方案、網(wǎng)站設(shè)計(jì)等建站排名服務(wù)。
秒殺會(huì)產(chǎn)生一個(gè)瞬間的高并發(fā),使用數(shù)據(jù)庫會(huì)增加數(shù)據(jù)庫的訪問壓力,也會(huì)降低訪問速度,所以我們應(yīng)該使用緩存,來降低數(shù)據(jù)庫的訪問壓力;
可以看出這里的操作和原來的下單是不一樣的:產(chǎn)生的秒殺預(yù)訂單不會(huì)馬上寫入數(shù)據(jù)庫,會(huì)先寫入緩存,等用戶支付成功時(shí),修改狀態(tài),寫入數(shù)據(jù)庫。
假設(shè)num是存儲(chǔ)在數(shù)據(jù)庫中的字段,保存了被秒殺產(chǎn)品的剩余數(shù)量。
if($num > 0){ //用戶搶購成功,記錄用戶信息 $num--; }
假設(shè)在一個(gè)并發(fā)量較高的場景,數(shù)據(jù)庫中num的值為1時(shí),可能同時(shí)會(huì)有多個(gè)進(jìn)程讀取到num為1,程序判斷符合條件,搶購成功,num減一。
這樣會(huì)導(dǎo)致商品超發(fā)的情況,本來只有10件可以搶購的商品,可能會(huì)有超過10個(gè)人搶到,此時(shí)num在搶購?fù)瓿芍鬄樨?fù)值。
解決該問題的方案由很多,可以簡單分為基于MySQL和redis的解決方案,redis的性能要由于mysql,因此可以承載更高的并發(fā)量,不過下面介紹的方案都是基于單臺(tái)mysql和redis的,更高的并發(fā)量需要分布式的解決方案,本文沒有涉及。
商品表 goods
CREATE TABLE `goods` ( `id` int(11) NOT NULL, `num` int(11) DEFAULT NULL, `version` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
搶購結(jié)果表 log
CREATE TABLE `log` ( `id` int(11) NOT NULL AUTO_INCREMENT, `good_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
①悲觀鎖
悲觀鎖的方案采用的是排他讀,也就是同時(shí)只能有一個(gè)進(jìn)程讀取到num的值。事務(wù)在提交或回滾之后,鎖會(huì)釋放,其他的進(jìn)程才能讀取。
該方案最簡單易懂,在對(duì)性能要求不高時(shí),可以直接采用該方案。要注意的是,SELECT … FOR UPDATE
要盡可能的使用索引,以便鎖定盡可能少的行數(shù);
排他鎖是在事務(wù)執(zhí)行結(jié)束之后才釋放的,不是讀取完成之后就釋放,因此使用的事務(wù)應(yīng)該盡可能的早些提交或回滾,以便早些釋放排它鎖。
$this->mysqli->begin_transaction(); $result = $this->mysqli->query("SELECT num FROM goods WHERE id=1 LIMIT 1 FOR UPDATE"); $row = $result->fetch_assoc(); $num = intval($row['num']); if($num > 0){ usleep(100); $this->mysqli->query("UPDATE goods SET num=num-1"); $affected_rows = $this->mysqli->affected_rows; if($affected_rows == 1){ $this->mysqli->query("INSERT INTO log(good_id) VALUES({$num})"); $affected_rows = $this->mysqli->affected_rows; if($affected_rows == 1){ $this->mysqli->commit(); echo "success:".$num; }else{ $this->mysqli->rollback(); echo "fail1:".$num; } }else{ $this->mysqli->rollback(); echo "fail2:".$num; } }else{ $this->mysqli->commit(); echo "fail3:".$num; }
②樂觀鎖
樂觀鎖的方案在讀取數(shù)據(jù)是并沒有加排他鎖,而是通過一個(gè)每次更新都會(huì)自增的version字段來解決,多個(gè)進(jìn)程讀取到相同num,然后都能更新成功的問題。在每個(gè)進(jìn)程讀取num的同時(shí),也讀取version的值,并且在更新num的同時(shí)也更新version,并在更新時(shí)加上對(duì)version的等值判斷。
假設(shè)有10個(gè)進(jìn)程都讀取到了num的值為1,version值為9,則這10個(gè)進(jìn)程執(zhí)行的更新語句都是UPDATE goods SET num=num-1,version=version+1 WHERE version=9
,
然而當(dāng)其中一個(gè)進(jìn)程執(zhí)行成功之后,數(shù)據(jù)庫中version的值就會(huì)變?yōu)?0,剩余的9個(gè)進(jìn)程都不會(huì)執(zhí)行成功,這樣保證了商品不會(huì)超發(fā),num的值不會(huì)小于0,但這也導(dǎo)致了一個(gè)問題,那就是發(fā)出搶購請求較早的用戶可能搶不到,反而被后來的請求搶到了。
$result = $this->mysqli->query("SELECT num,version FROM goods WHERE id=1 LIMIT 1"); $row = $result->fetch_assoc(); $num = intval($row['num']); $version = intval($row['version']); if($num > 0){ usleep(100); $this->mysqli->begin_transaction(); $this->mysqli->query("UPDATE goods SET num=num-1,version=version+1 WHERE version={$version}"); $affected_rows = $this->mysqli->affected_rows; if($affected_rows == 1){ $this->mysqli->query("INSERT INTO log(good_id) VALUES({$num})"); $affected_rows = $this->mysqli->affected_rows; if($affected_rows == 1){ $this->mysqli->commit(); echo "success:".$num; }else{ $this->mysqli->rollback(); echo "fail1:".$num; } }else{ $this->mysqli->rollback(); echo "fail2:".$num; } }else{ echo "fail3:".$num; }
③where條件(原子操作)
悲觀鎖的方案保證了數(shù)據(jù)庫中num的值在同一時(shí)間只能被一個(gè)進(jìn)程讀取并處理,也就是并發(fā)的讀取進(jìn)程到這里要排隊(duì)依次執(zhí)行。
樂觀鎖的方案雖然num的值可以被多個(gè)進(jìn)程同時(shí)讀取到,但是更新操作中version的等值判斷可以保證并發(fā)的更新操作在同一時(shí)間只能有一個(gè)更新成功。
還有一種更簡單的方案,只在更新操作時(shí)加上num>0的條件限制即可。通過where條件限制的方案雖然看似和樂觀鎖方案類似,都能夠防止超發(fā)問題的出現(xiàn),但在num較大時(shí)的表現(xiàn)還是有很大區(qū)別的。
假如此時(shí)num為10,同時(shí)有5個(gè)進(jìn)程讀取到了num=10,對(duì)于樂觀鎖的方案由于version字段的等值判斷,這5個(gè)進(jìn)程只會(huì)有一個(gè)更新成功,這5個(gè)進(jìn)程執(zhí)行完成之后num為9;
對(duì)于where條件判斷的方案,只要num>0都能夠更新成功,這5個(gè)進(jìn)程執(zhí)行完成之后num為5。
$result = $this->mysqli->query("SELECT num FROM goods WHERE id=1 LIMIT 1"); $row = $result->fetch_assoc(); $num = intval($row['num']); if($num > 0){ usleep(100); $this->mysqli->begin_transaction(); $this->mysqli->query("UPDATE goods SET num=num-1 WHERE num>0"); $affected_rows = $this->mysqli->affected_rows; if($affected_rows == 1){ $this->mysqli->query("INSERT INTO log(good_id) VALUES({$num})"); $affected_rows = $this->mysqli->affected_rows; if($affected_rows == 1){ $this->mysqli->commit(); echo "success:".$num; }else{ $this->mysqli->rollback(); echo "fail1:".$num; } }else{ $this->mysqli->rollback(); echo "fail2:".$num; } }else{ echo "fail3:".$num; }
①基于watch的樂觀鎖方案
watch用于監(jiān)視一個(gè)(或多個(gè)) key ,如果在事務(wù)執(zhí)行之前這個(gè)(或這些) key 被其他命令所改動(dòng),那么事務(wù)將被打斷。
這種方案跟mysql中的樂觀鎖方案類似,具體表現(xiàn)也是一樣的。
$num = $this->redis->get('num'); if($num > 0) { $this->redis->watch('num'); usleep(100); $res = $this->redis->multi()->decr('num')->lPush('result',$num)->exec(); if($res == false){ echo "fail1"; }else{ echo "success:".$num; } }else{ echo "fail2"; }
②基于list的隊(duì)列方案
基于隊(duì)列的方案利用了redis出隊(duì)操作的原子性,搶購開始之前首先將商品編號(hào)放入響應(yīng)的隊(duì)列中,在搶購時(shí)依次從隊(duì)列中彈出操作,這樣可以保證每個(gè)商品只能被一個(gè)進(jìn)程獲取并操作,不存在超發(fā)的情況。
該方案的優(yōu)點(diǎn)是理解和實(shí)現(xiàn)起來都比較簡單,缺點(diǎn)是當(dāng)商品數(shù)量較多是,需要將大量的數(shù)據(jù)存入到隊(duì)列中,并且不同的商品需要存入到不同的消息隊(duì)列中。
public function init(){ $this->redis->del('goods'); for($i=1;$i<=10;$i++){ $this->redis->lPush('goods',$i); } $this->redis->del('result'); echo 'init done'; } public function run(){ $goods_id = $this->redis->rPop('goods'); usleep(100); if($goods_id == false) { echo "fail1"; }else{ $res = $this->redis->lPush('result',$goods_id); if($res == false){ echo "writelog:".$goods_id; }else{ echo "success".$goods_id; } } }
③基于decr返回值的方案
如果我們將剩余量num設(shè)置為一個(gè)鍵值類型,每次先get之后判斷,然后再decr是不能解決超發(fā)問題的。
但是redis中的decr操作會(huì)返回執(zhí)行后的結(jié)果,可以解決超發(fā)問題。我們首先get到num的值進(jìn)行第一步判斷,避免每次都去更新num的值,然后再對(duì)num執(zhí)行decr操作,并判斷decr的返回值,如果返回值不小于0,這說明decr之前是大于0的,用戶搶購成功。
public function run(){ $num = $this->redis->get('num'); if($num > 0) { usleep(100); $retNum = $this->redis->decr('num'); if($retNum >= 0){ $res = $this->redis->lPush('result',$retNum); if($res == false){ echo "writeLog:".$retNum; }else{ echo "success:".$retNum; } }else{ echo "fail1"; } }else{ echo "fail2"; } }
④基于setnx的排它鎖方案
redis沒有像mysql中的排它鎖,但是可以通過一些方式實(shí)現(xiàn)排它鎖的功能,就類似php使用文件鎖實(shí)現(xiàn)排它鎖一樣。
setnx實(shí)現(xiàn)了exists和set兩個(gè)指令的功能,若給定的key已存在,則setnx不做任何動(dòng)作,返回0;若key不存在,則執(zhí)行類似set的操作,返回1。
我們設(shè)置一個(gè)超時(shí)時(shí)間timeout,每隔一定時(shí)間嘗試setnx操作,如果設(shè)置成功就是獲得了相應(yīng)的鎖,執(zhí)行num的decr操作,操作完成刪除相應(yīng)的key,模擬釋放鎖的操作。
public function run(){ do { $res = $this->redis->setnx("numKey",1); $this->timeout -= 100; usleep(100); }while($res == 0 && $this->timeout>0); if($res == 0){ echo 'fail1'; }else{ $num = $this->redis->get('num'); if($num > 0) { $this->redis->decr('num'); usleep(100); $res = $this->redis->lPush('result',$num); if($res == false){ echo "fail2"; }else{ echo "success:".$num; } }else{ echo "fail3"; } $this->redis->del("numKey"); } }
以上就是關(guān)于“如何解決PHP高并發(fā)問題”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對(duì)大家有幫助,若想了解更多相關(guān)的知識(shí)內(nèi)容,請關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。
網(wǎng)站題目:如何解決PHP高并發(fā)問題
文章URL:http://www.rwnh.cn/article28/jdcpjp.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供App設(shè)計(jì)、電子商務(wù)、網(wǎng)站收錄、品牌網(wǎng)站建設(shè)、服務(wù)器托管、網(wǎng)站制作
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)