php mysql lock tables 使用有感
mysql 的 表鎖 lock tables 感覺就像一個 封閉的空間
mysql發現 lock tables 命令的時候,會將帶有鎖標記的表(table) 帶入封閉空間,直到 出現 unlock tables 命令 或 線程結束, 才關閉封閉空間。
進入封閉空間時 , 僅僅只有鎖標記的表(table) 可以在里面使用,其他表無法使用。
鎖標記 分為 read 和 write 下面是 兩種 鎖的區別
--------------------------------------------------------------------
//如 將 table1 設為read鎖, table2 設為write鎖, table3 設為read鎖
lock tables [table1] read,[table2] write,[table3] read;
----------------------------------------------------------------------
//執行到這里時,進入封閉空間。
1. table1 僅允許[所有人]讀,[空間外]如需寫、更新要等待[空間退出],[空間內]如需寫、更新會引發mysql報錯。
2. table2 僅允許[空間內]讀寫更新,[空間外]如需寫、更新要等待[空間退出]。
3. table3 僅允許[所有人]讀,[空間外]如需寫、更新要等待[空間退出],[空間內]如需寫、更新會引發mysql報錯。
----------------------------------------------------------------------
//執行到這里時,退出封閉空間,釋放所有表鎖
unlock tables
----------------------------------------------------------------------
當前線程關閉時,自動退出封閉空間,釋放所有表鎖,無論有沒有執行 unlock tables
上面一堆東西感覺很亂,下面我們看個實例吧。
在某個地方看到有個例子,具體描述類似如下:商店現在某商品只有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。這確實沒有任何錯誤,邏輯也對。如 果請求是一個接一個地產生的,那么什么問題都沒有,但當一些並發情況(paperen也不想用這種專業的名詞,其實就是上面那個例子的情況,在相差不明顯 的時間內有多個請求產生)出現時就可能出現一些無厘頭的問題了。你想啊,是不是可能存在一種情況,A剛發出請求,腳本處理到update之前B又發出請 求,那么現在庫存依然還有1,因為A的update還沒有執行呢,所以$number不少於0,這次完了,B也下單了,於是庫存變成-1了(假設原來只有 1件),確實是一個荒謬而且比較搞笑的結果。
出現問題的原因很明顯,就是忽略了這種並發情況的考慮,處理下訂應該是種隊列方式,也就是先來先得,就是說在執行這個下訂動作是要排隊的,前面的那 個先下訂然后后者才能下訂,當然當后者下訂前才再判斷庫存的數量。那么怎樣解決這個問題呢,在程序層面上貌似真的沒有方法去解決這個問題(paperen 可沒想到代碼上的解決方案,有思路的可以留個言),所以在此才提到鎖表的概念,你想啊,上面出現這個問題的歸根於沒有控制一個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" ); } |
關於lock自己可以再斟酌一下,因為第二個參數這樣弄看上去並不太舒服。嗯哼~那怎測試呢?paperen使用jmeter進行測試結果
關於jmeter可以在http://jakarta.apache.org/site/downloads/downloads_jmeter.cgi 這里下載,在邪惡的人手中可以是一個恐怖的工具在善良的人手中是一個友好的工具。
您需要創建兩個線程,其實就是對服務器發出兩個請求。
具體配置paperen在此不說,我導出了一個計划文件,大家可以試着打開就能看到paperen是怎測試的了。http://iamlze.cn/demo/locktable/locktable.jmx
保存下來然后導入必需調整一下你本地測試的路徑,最后ctrl+R(運行),在線程下查看結果樹就有請求的回應信息了。
首先測試不加鎖表的情況(就是一開始不加lock與unlock操作的代碼)看看兩個線程出來的結果。
都是ok~~再看數據庫
然后將number改回1,再將lock與unlock,鎖表操作加上,再運行。
好吧,數據表就不用看了吧,結果已經很明顯了,再前一個請求對表操作完成之前,之后那些請求都要在等待,直到前面請求完成了才能操作,也就是隊列的味道。
老實說mysql的事務也需要下點功夫研究一下,paperen關於鎖表的了解也就是在查看事務的過程中產生的,在高級的應用過程中這種技術就更加重要,更加嚴謹的邏輯代碼與嚴謹的數據庫管理才能更進一步保證數據的真實與准確性。真是后知后覺。