商品秒殺問題的解決方案

2021-08-14 09:58:13 字數 4303 閱讀 7853

假設num是儲存在資料庫中的字段,儲存了被秒殺產品的剩餘數量。

if($num > 0)
假設在乙個併發量較高的場景,資料庫中num的值為1時,可能同時會有多個程序讀取到num為1,程式判斷符合條件,搶購成功,num減一。這樣會導致商品超發的情況,本來只有10件可以搶購的商品,可能會有超過10個人搶到,此時num在搶購完成之後為負值。

解決該問題的方案由很多,可以簡單分為基於mysql和redis的解決方案,redis的效能要由於mysql,因此可以承載更高的併發量,不過下面介紹的方案都是基於單台mysql和redis的,更高的併發量需要分布式的解決方案,本文沒有涉及。

商品表 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))");

$affected_rows = $this->mysqli->affected_rows;

if($affected_rows == 1)else

}else

}else

樂觀鎖的方案在讀取資料是並沒有加排他鎖,而是通過乙個每次更新都會自增的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)");

$affected_rows = $this->mysqli->affected_rows;

if($affected_rows == 1))");

$affected_rows = $this->mysqli->affected_rows;

if($affected_rows == 1)else

}else

}else

悲觀鎖的方案保證了資料庫中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))");

$affected_rows = $this->mysqli->affected_rows;

if($affected_rows == 1)else

}else

}else

watch用於監視乙個(或多個) key ,如果在事務執行之前這個(或這些) key 被其他命令所改動,那麼事務將被打斷。這種方案跟mysql中的樂觀鎖方案類似,具體表現也是一樣的。

$num

=$this

->redis->get('num');

if($num

>

0) else

}else

基於佇列的方案利用了redis出隊操作的原子性,搶購開始之前首先將商品編號放入響應的佇列中,在搶購時依次從佇列中彈出操作,這樣可以保證每個商品只能被乙個程序獲取並操作,不存在超發的情況。該方案的優點是理解和實現起來都比較簡單,缺點是當商品數量較多是,需要將大量的資料存入到佇列中,並且不同的商品需要存入到不同的訊息佇列中。

public

function

init

() $this->redis->del('result');

echo

'init done';

}public

function

run()elseelse

}}

如果我們將剩餘量num設定為乙個鍵值型別,每次先get之後判斷,然後再decr是不能解決超發問題的。但是redis中的decr操作會返回執行後的結果,可以解決超發問題。我們首先get到num的值進行第一步判斷,避免每次都去更新num的值,然後再對num執行decr操作,並判斷decr的返回值,如果返回值不小於0,這說明decr之前是大於0的,使用者搶購成功。

public

function

run()else

}else

}else

}

redis沒有像mysql中的排它鎖,但是可以通過一些方式實現排它鎖的功能,就類似php使用檔案鎖實現排它鎖一樣。

setnx實現了exists和set兩個指令的功能,若給定的key已存在,則setnx不做任何動作,返回0;若key不存在,則執行類似set的操作,返回1。我們設定乙個超時時間timeout,每隔一定時間嘗試setnx操作,如果設定成功就是獲得了相應的鎖,執行num的decr操作,操作完成刪除相應的key,模擬釋放鎖的操作。

public function run()while($res==0

&&$this

->timeout>

0); if($res

==0)elseelse

}else

$this

->redis->del("numkey");

}}

php高併發秒殺解決方案

在秒殺 搶火車票等地方,我們通常用遇到這樣高併發的問題,下面提供了四種解決方案 1 使用檔案鎖 php view plain copy fp fopen order.lock r if flock fp,lock ex fclose fp 2 使用訊息佇列 我們常用到memcacheq radis。...

PHP 高併發秒殺解決方案

本文提供 php 高併發秒殺解決方案 附加三個案例說明 普通流程,使用檔案鎖,使用redis訊息佇列 1 正常流程,不做任何高併發處理 如下 mysqli new mysqli localhost root secondkill if mysqli connect errno mysqli set ...

秒殺的可行的簡單的解決方案

現在的秒殺很流行,對數百萬計的使用者同時訪問伺服器的壓力一定很大,簡單的辦法就是把商程品先快取起來,根據伺服器的具體情況,在後台在秒殺的前一時刻先快取所有要秒殺的商品,這一點通過後台執行緒很容易實現,秒殺開始時從快取記憶體中取資料。例如 要秒殺ipad,可以 50臺,那麼在秒殺之前先把這個資訊放到快...