在某個地方看到有個例子,具體描述類似如下:商店現在某商品只有1件庫存,然後A與B在網上進行下訂,A與B幾乎同時(或許也就差幾毫秒,A比B快那麼一點點)進行。
很明顯是隻有A才能成功下單的,B則會收到庫存不足的提示,但是作爲放置在服務端的那個頁面(或者稱爲腳本程序)我們得怎樣去處理這個問題呢?或者我先放出一段代碼吧。
代碼如下 | 複製代碼 |
$sql = "select number from goods where id=1"; |
這部分代碼除了缺少一定註釋外都寫得沒錯,當然$db是一個操作數據庫的類,我只是將大部分方法封裝了,這裏的邏輯也是很明顯了。
先獲取id爲1這個東東的庫存數,看看是否爲0,如果爲0就訂購不成功了,如果大於0則將庫存減1然後提示ok。這確實沒有任何錯誤,邏輯也對。如果請求是一個接一個地產生的,那麼什麼問題都沒有,但當一些併發情況出現時就可能出現一些無厘頭的問題了。你想啊,是不是可能存在一種情況,A剛發出請求,腳本處理到update之前B又發出請求,那麼現在庫存依然還有1,因爲A的update還沒有執行呢,所以$number不少於0,這次完了,B也下單了,於是庫存變成-1了(假設原來只有1件),確實是一個荒謬而且比較搞笑的結果。
出現問題的原因很明顯,就是忽略了這種併發情況的考慮,處理下訂應該是種隊列方式,也就是先來先得,就是說在執行這個下訂動作是要排隊的,前面的那個先下訂然後後者才能下訂,當然當後者下訂前纔再判斷庫存的數量。那麼怎樣解決這個問題呢,在程序層面上貌似真的沒有方法去解決這個問題,所以在此才提到鎖表的概念,上面出現這個問題的歸根於沒有控制一個select number的先後順序(或者可以這麼說吧),因爲在A執行update之前你又允許B去查詢庫存,當然結果還是1,至少要等待A更新庫存後才允許其他人的任何操作,也就是對goods表進行一個排隊操作,對goods表進行鎖定。
說到這裏,請不要以爲鎖表有多麼高深,其實它就是一條sql
LOCK TABLE `table` [READ|WRITE]
解鎖
UNLOCK TABLES;
引用專業的描述是
LOCK TABLES爲當前線程鎖定表。 UNLOCK TABLES釋放被當前線程持有的任何鎖。當線程發出另外一個LOCK TABLES時,或當服務器的連接被關閉時,當前線程鎖定的所有表會自動被解鎖。
如果一個線程獲得在一個表上的一個READ鎖,該線程和所有其他線程只能從表中讀。 如果一個線程獲得一個表上的一個WRITE鎖,那麼只有持鎖的線程READ或WRITE表,其他線程被阻止。
已經是有種隊列的味道,對不,所以解決方案很簡單嘛,在select前加鎖,執行完後面邏輯代碼後解鎖。或許有沒有人會有一個疑問,就是如果萬一鎖表後線程就斷掉了那麼是不是就一直鎖表了,這個確實是可能存在但是既然你想到了那麼數據庫的設計人員也一定考慮到了,可以告訴你關於unlock的一些資料:當線程發出另一個 LOCK TABLES,或當與服務器的連接被關閉時,被當前線程鎖定的所有表將被自動地解鎖。這下放心了吧。
好,看下改進後的代碼。
代碼如下 | 複製代碼 |
$db->lock( 'goods', 2 ); $sql = "select number from goods where id=1"; $number = intval( $db->result( $db->query( $sql ), 0 ) ); if ( $number > 0 ) { sleep( 2 ); $sql = "update goods set number=number-1 where id = 1"; if ( $db->query( $sql ) ) { echo 'Ok!Here you are!'; } else { echo 'Sorry!Something go wrong!Try it again.'; } } else { echo 'No more!you are so late!'; } $db->unlock(); |
只加了兩行代碼,不過也不能這麼說,因爲paperen我修改了自己那個操作數據庫的類,加了兩個方法lock與unlock,其實這兩個方法也很簡單。
代碼如下 | 複製代碼 |
/** * 鎖表 * @param string $table 表名 * @param int $type 讀鎖1還是寫鎖2 */ public function lock( $table, $type = 1 ) { $type = ( $type == 1 ) ? 'READ' : 'WRITE'; $this->query( "LOCK TABLE `$table` $type" ); } /** * 解鎖 */ public function unlock() { $this->query( "UNLOCK TABLES" ); } |