數據庫進階之路(五) - MySQL行鎖深刻研究

 

因爲業務邏輯的須要,必須對數據表的一行或多行加入行鎖,舉個最簡單的例子,圖書借閱系統:假設id=1的這本書庫存爲1,可是有2我的同時來借這本書,此處的邏輯爲:mysql

SELECT  restnum FROM book WHERE id =1  ;  --若是restnum大於0,執行update
UPDATE  book SET restnum=restnum-1 WHERE id=1;

問題來了,當2我的同時借的時候,有可能第一我的執行select語句的時候,第二我的插了進來,在第一我的沒來得及更新book表的時候,第二我的就查到了數據,但這是一個髒數據,由於第一我的會把restnum值減1,所以第二我的原本應該是查到id=1的書restnum爲0了,所以不會執行update,而會告訴它id=1的書沒有庫存 了,但是數據庫哪懂這些,數據庫只負責執行一條條SQL語句,它才無論中間有沒有其餘sql語句插進來,它也不知道要把一個session的sql語句執行完再執行另外一個session的。所以會致使併發的時候restnum最後的結果爲-1,顯然這是不合理的,因此出現了鎖的概念,Mysql使用innodb引擎能夠經過索引對數據行加鎖。以上借書的語句變爲:sql

BEGIN;
SELECT restnum FROM book WHERE id =1 FOR UPDATE; -- 給id=1的行加上排它鎖且id有索引
UPDATE  book SET restnum=restnum-1 WHERE  id=1;
Commit;

這樣,第二我的執行到select語句的時候就會處於等待狀態直到第一我的執行commit。從而保證了第二我的不會讀到第一我的修改前的數據。 那這樣是否是萬無一失了呢,答案是否認的。看下面的例子。數據庫

跟我一步一步來,先創建表,其中num字段加了索引session

CREATE TABLE 'book' (
  'id' INT(11) NOT NULL AUTO_INCREMENT,
  'num' INT(11) DEFAULT NULL,
  'name' VARCHAR(0) DEFAULT NULL,
  PRIMARY KEY ('id'),
  KEY 'asd' ('num')
) ENGINE=InnoDB DEFAULT CHARSET=gbk

而後插入數據,運行併發

INSERT INTO book(num) VALUES(11),(11),(11),(11),(11);
INSERT INTO book(num) VALUES(22),(22),(22),(22),(22);

而後打開2個mysql控制檯窗口,其實就是創建2個session作併發操做測試

--------------------------------------------------------------------------優化

在第一個session裏運行:spa

BEGIN;
SELECT * FROM book WHERE num=11 FOR UPDATE;

出現結果:設計

 | id | num | name|   
 | 11 | 11   | NULL |   
 | 12 | 11   | NULL |   
 | 13 | 11   | NULL |   
 | 14 | 11   | NULL |  
 | 15 | 11   | NULL |  
 5 rows in set

而後在第二個session裏運行:rest

BEGIN;
SELECT * FROM book WHERE num=22 FOR UPDATE;

出現結果:

| id| num | name | 
| 16 | 22 | NULL | 
| 17 | 22 | NULL |  
| 18 | 22 | NULL |  
| 19 | 22 | NULL |  
| 20 | 22 | NULL | 
5 rows in set

好了,到這裏什麼問題都沒有,是吧,但是接下來問題就來了,你們請看: 回到第一個session,運行:

UPDATE book SET name='abc' WHERE num=11;

 --------------------------------------------------------------------------

問題來了,session居然處於等待狀態,但是num=11的行不是被第一個session本身鎖住的麼,爲何不能更新呢?好了,打這裏你們也許有本身的答案,先別急,再請看一下操做。
把2個session都關閉,而後運行:

DELETE FROM book WHERE num=11 LIMIT 3;
DELETE FROM book WHERE num=22 LIMIT 3;

其實就是把num=11和22的記錄各刪去3行, 而後重複分割線之間的操做 居然發現,運行update book set name=’abc’ where num=11;後,有結果出現了,說明沒有被鎖住, 這是爲何呢,難道2行數據和5行數據,對MySQL來講,會產生鎖行和鎖表兩種狀況嗎。通過跟網友討論和翻閱資料,仔細分析後發現: 在以上實驗數據做爲測試數據的狀況下,因爲num字段重複率過高,只有2個值,分別是11和12.而數據量相對於這兩個值來講倒是比較大的,是10條,5倍的關係。 那麼mysql在解釋sql的時候,會忽略索引,由於它的優化器發現:即便使用了索引,仍是要作全表掃描,故而放棄了索引,也就沒有使用行鎖,卻使用了表鎖。簡單的講,就是MYSQL無視了你的索引,它以爲與其行鎖,還不如直接表鎖,畢竟它以爲表鎖所花的代價比行鎖來的小。以上問題即使你使用了force index強制索引,結果仍是同樣,永遠都是表鎖。 因此mysql 的行鎖用起來並非那麼爲所欲爲的,必需要考慮索引。再看下面的例子:

SELECT id FROM items WHERE id IN (SELECT id FROM items WHERE id < 6) FOR UPDATE; --id字段加了索引
SELECT id FROM items WHERE id IN (1,2,3,4,5) FOR UPDATE;

大部分會認爲結果同樣沒什麼區別,其實差異大了,區別就是第一條sql語句會產生表鎖,而第二個sql語句是行鎖,爲何呢?由於第一個sql語句用了子查詢外圍查詢故而沒使用索引,致使表鎖。

好了,回到借書的例子,因爲id是惟一的,因此沒什麼問題,可是若是有些表出現了索引有重複值,而且mysql會強制使用表鎖的狀況,那怎麼辦呢?通常來講只有從新設計表結構和用新的SQL語句實現業務邏輯,可是其實上面借書的例子還有一種辦法。請看下面代碼:

SET sql_mode=
'STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';
BEGIN;
SELECT restnum FROM book WHERE id =1   ; --取消排它鎖, 設置restnum爲unsigned
UPDATE  book SET restnum=restnum-1 WHERE  ;
IF(UPDATE執行成功) commit;
ELSE  ROLLBACK;

 

上面是個小技巧,經過把數據庫模式臨時設置爲嚴格模式,當restnum被更新爲-1的時候,因爲restnum是unsigned類型的,所以update會執行失敗,不管第二個session作了什麼數據庫操做,都會被回滾,從而確保了數據的正確性,這個目的只是爲了防止併發的時候極小機率出現的2個session的sql語句嵌套執行致使數據髒讀。固然最好的辦法仍是修改表結構和sql語句,讓MYSQL經過索引來加行鎖。 MySQL測試版本爲5.0.75-log和5.1.36-community.

相關文章
相關標籤/搜索