標籤:
如果在對某行記錄的更新時不採取任何防範措施,在多線程訪問時,就容易出現庫存為負數的錯誤.
以下用php、mysql,apache ab工具舉例說明:
mysql表結構
CREATE TABLE `yxt_test_concurrence` ( `id` int(11) NOT NULL AUTO_INCREMENT, `value` int(11) NOT NULL COMMENT ‘庫存‘, PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT=‘庫存表‘;
CREATE TABLE `yxt_test_pv` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `val` int(255) DEFAULT NULL COMMENT ‘該線程讀取到的庫存數量‘, PRIMARY KEY (`id`)) ENGINE=MyISAM AUTO_INCREMENT=351 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT=‘訪問記錄表,每次訪問都增加一條記錄,並記錄此次訪問時的庫存數‘;
在庫存表中存入類比庫存500個.
在此,為方便,php採用TP架構:
public function tc(){ $this->tc = M("test_concurrence");//類比商品的剩餘數量 $this->pv = M("test_pv");//類比訪問次數 $res=$this->tc->field(‘value‘)->find(1);//查到的剩餘數量 $value=$res[‘value‘]; if($value>0){//如果大於0,則進行下面的邏輯 $this->pv->data(array(‘val‘=>$value))->add();//這個是用來記錄訪問的次數,並記錄此次訪問時的庫存數
M()->execute("UPDATE `yxt_test_concurrence` SET `value`=`value` - 1 WHERE `id` = 1"); //商品數量減1
}
}
使用ab工具類比並發訪問:
C:\Users\chenhui>ab -c 50 -n 500 http://study.com/course/Course/tc/This is ApacheBench, Version 2.3 <$Revision: 1554214 $>Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/Licensed to The Apache Software Foundation, http://www.apache.org/Benchmarking studyyxtcmf.com (be patient)Completed 100 requestsCompleted 200 requestsCompleted 300 requestsCompleted 400 requestsCompleted 500 requestsFinished 500 requestsServer Software: Apache/2.4.9Server Hostname: studyyxtcmf.comServer Port: 80Document Path: /course/Course/tc/Document Length: 25786 bytesConcurrency Level: 50Time taken for tests: 60.035 secondsComplete requests: 500Failed requests: 450 (Connect: 0, Receive: 0, Length: 450, Exceptions: 0)Total transferred: 12973630 bytesHTML transferred: 12785130 bytesRequests per second: 8.33 [#/sec] (mean)Time per request: 6003.543 [ms] (mean)Time per request: 120.071 [ms] (mean, across all concurrent requests)Transfer rate: 211.03 [Kbytes/sec] receivedConnection Times (ms) min mean[+/-sd] median maxConnect: 0 1 2.1 1 34Processing: 781 5915 1578.6 5996 12272Waiting: 765 5901 1581.8 5983 12261Total: 783 5916 1578.4 5997 12272Percentage of the requests served within a certain time (ms) 50% 5997 66% 6385 75% 6707 80% 6850 90% 7387 95% 8402 98% 9734 99% 10300 100% 12272 (longest request)
查看資料庫記錄:
SELECT * from yxt_test_pv;--截取一段記錄(左邊是第幾次訪問,右側是當次訪問看到的庫存)| 338 | 164 || 339 | 164 || 340 | 163 || 341 | 162 || 342 | 162 || 343 | 162 || 344 | 162 || 345 | 157 || 346 | 156 || 347 | 156 || 348 | 153 || 349 | 155 || 350 | 151 |
可以發現在341-343次讀取的庫存數量是一樣的,在庫存還很多的情況時,並不會出現問題:因為程式中減庫存的邏輯,是當前庫存量減去1.但是庫存不多的時候,就很可能出現問題,比如庫存只有一個了,而此時有多個線程查詢到此時還有一個庫存,因為1>0滿足條件,所以庫存減1,多個線程都對當前庫存減1,最後就多減了庫存,出現負數,這是不允許的.
所以一定要採取措施.
我認為,總的原則是:對於某一個時刻的庫存,只允許一個會話去修改.要滿足此條件.有兩種選擇:
1.對於某一個時刻的庫存,只允許一個會話去讀取(鎖機制).待鎖被釋放後,其他會話才可以讀取庫存.
2.對於某一個時刻的庫存,設定版本(即增加一個版本欄位,用於比較.我對版本的理解是刻個記號),更新庫存時要判斷版本是否發生變化,若沒發生變化,則更新庫存的同時,更新版本號碼.若更新庫存時發現版本發生變化了,那一定是有別的線程早已對庫存修改,此情況下就放棄修改.
選擇1.使用mysql的鎖機制.(悲觀鎖)
public function tc(){ $this->tc = M("test_concurrence");//類比商品的剩餘數量 $this->pv = M("test_pv");//類比訪問次數 //對錶加鎖,注意,如果加鎖過程中要操作多個表,要對這幾個表都加鎖,否則會報錯
//mysql> lock table yxt_test_concurrence read;--只鎖了一張表
//Query OK, 0 rows affected (0.00 sec)
//mysql> SELECT * from yxt_test_pv;--讀取沒有被鎖的表
//ERROR 1100 (HY000): Table ‘yxt_test_pv‘ was not locked with LOCK TABLES--報錯,提示查詢的表沒有被鎖住
M()->execute("lock tables yxt_test_concurrence write,yxt_test_pv write;"); $res=$this->tc->field(‘value‘)->find(1);//查到的剩餘數量 $value=$res[‘value‘]; if($value>0){//如果大於0,則進行下面的邏輯 $this->pv->data(array(‘val‘=>$value))->add();//這個是用來記錄訪問的次數 M()->execute( "UPDATE `yxt_test_concurrence` SET `value`=`value` - 1 WHERE `id` = 1"); //商品數量減1 } //解鎖 M()->execute("unlock tables"); }
採用鎖機制,可以嚴格控制庫存數量的變化,但是採用鎖會增加資料庫的開銷.
選擇2.版本控制(樂觀鎖)
樂觀鎖,是假定事務之間是互不干擾的,事務在訪問資料的時候,並不會擷取鎖,但是,在提交前,每個事務都要確保其他事務並沒有修改他讀取到的資料.如果在更新資料時發現其他事務已經修改了資料,則復原提交.樂觀鎖經常用於"低爭用資料結構"的情境中.當衝突特別少的時候,事務可以在完成時,不需要管理鎖的開銷及等待其他事務釋放鎖,這可以帶來更高的吞吐率.但是,如果對於資料的爭用特別頻繁,重新開啟一個新事務的開銷會明顯影響效能.
通常認為,其他並發控制方法,在此情況下會有更好的表現,然而,基於悲觀鎖的方法,會導致較差的效能.因為即使死結可以避免,"鎖"仍會極大的影響並發效能.(我想應該是因為會話被阻塞,從而導致只能串列訪問資料庫)
以上定義摘自wiki:https://en.wikipedia.org/wiki/Optimistic_concurrency_control
這種情況下,如果並發訪問,則修改失敗的幾率會較高,
舉例:在熱銷產品情境下則容易出現購買失敗的情況.這對使用者的體驗是不好的.因為這意味著又要重新嘗試一次.
小結:應該採取哪一種鎖,應根據實際情境來權衡利弊,如果更新的很頻繁,那應該使用悲觀鎖.此刻需要考慮的問題是:如何解決並發問題.如果很少更新,則使用樂觀鎖更為方便省事.
高並發訪問mysql時的問題(一):庫存超減