您好,登錄后才能下訂單哦!
這篇文章主要介紹了如何使用PHP實現商品秒殺,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。
引言
假設num是存儲在數據庫中的字段,保存了被秒殺產品的剩余數量。
if($num > 0){ //用戶搶購成功,記錄用戶信息 $num--; }
假設在一個并發量較高的場景,數據庫中num的值為1時,可能同時會有多個進程讀取到num為1,程序判斷符合條件,搶購成功,num減一。這樣會導致商品超發的情況,本來只有10件可以搶購的商品,可能會有超過10個人搶到,此時num在搶購完成之后為負值。
解決該問題的方案由很多,可以簡單分為基于mysql和redis的解決方案,redis的性能要由于mysql,因此可以承載更高的并發量,不過下面介紹的方案都是基于單臺mysql和redis的,更高的并發量需要分布式的解決方案,本文沒有涉及。
基于mysql的解決方案
商品表 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
搶購結果表 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
悲觀鎖
悲觀鎖的方案采用的是排他讀,也就是同時只能有一個進程讀取到num的值。事務在提交或回滾之后,鎖會釋放,其他的進程才能讀取。該方案最簡單易懂,在對性能要求不高時,可以直接采用該方案。要注意的是,SELECT … FOR UPDATE要盡可能的使用索引,以便鎖定盡可能少的行數;排他鎖是在事務執行結束之后才釋放的,不是讀取完成之后就釋放,因此使用的事務應該盡可能的早些提交或回滾,以便早些釋放排它鎖。
$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; }
樂觀鎖
樂觀鎖的方案在讀取數據是并沒有加排他鎖,而是通過一個每次更新都會自增的version字段來解決,多個進程讀取到相同num,然后都能更新成功的問題。在每個進程讀取num的同時,也讀取version的值,并且在更新num的同時也更新version,并在更新時加上對version的等值判斷。假設有10個進程都讀取到了num的值為1,version值為9,則這10個進程執行的更新語句都是UPDATE goods SET num=num-1,version=version+1 WHERE version=9
,然而當其中一個進程執行成功之后,數據庫中version的值就會變為10,剩余的9個進程都不會執行成功,這樣保證了商品不會超發,num的值不會小于0,但這也導致了一個問題,那就是發出搶購請求較早的用戶可能搶不到,反而被后來的請求搶到了。
$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條件(原子操作)
悲觀鎖的方案保證了數據庫中num的值在同一時間只能被一個進程讀取并處理,也就是并發的讀取進程到這里要排隊依次執行。樂觀鎖的方案雖然num的值可以被多個進程同時讀取到,但是更新操作中version的等值判斷可以保證并發的更新操作在同一時間只能有一個更新成功。
還有一種更簡單的方案,只在更新操作時加上num>0的條件限制即可。通過where條件限制的方案雖然看似和樂觀鎖方案類似,都能夠防止超發問題的出現,但在num較大時的表現還是有很大區別的。假如此時num為10,同時有5個進程讀取到了num=10,對于樂觀鎖的方案由于version字段的等值判斷,這5個進程只會有一個更新成功,這5個進程執行完成之后num為9;對于where條件判斷的方案,只要num>0都能夠更新成功,這5個進程執行完成之后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; }
基于redis的解決方案
基于watch的樂觀鎖方案
watch用于監視一個(或多個) key ,如果在事務執行之前這個(或這些) key 被其他命令所改動,那么事務將被打斷。這種方案跟mysql中的樂觀鎖方案類似,具體表現也是一樣的。
$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的隊列方案
基于隊列的方案利用了redis出隊操作的原子性,搶購開始之前首先將商品編號放入響應的隊列中,在搶購時依次從隊列中彈出操作,這樣可以保證每個商品只能被一個進程獲取并操作,不存在超發的情況。該方案的優點是理解和實現起來都比較簡單,缺點是當商品數量較多是,需要將大量的數據存入到隊列中,并且不同的商品需要存入到不同的消息隊列中。
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設置為一個鍵值類型,每次先get之后判斷,然后再decr是不能解決超發問題的。但是redis中的decr操作會返回執行后的結果,可以解決超發問題。我們首先get到num的值進行第一步判斷,避免每次都去更新num的值,然后再對num執行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中的排它鎖,但是可以通過一些方式實現排它鎖的功能,就類似php使用文件鎖實現排它鎖一樣。
setnx實現了exists和set兩個指令的功能,若給定的key已存在,則setnx不做任何動作,返回0;若key不存在,則執行類似set的操作,返回1。我們設置一個超時時間timeout,每隔一定時間嘗試setnx操作,如果設置成功就是獲得了相應的鎖,執行num的decr操作,操作完成刪除相應的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"); } }
感謝你能夠認真閱讀完這篇文章,希望小編分享的“如何使用PHP實現商品秒殺”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業資訊頻道,更多相關知識等著你來學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。