在並發環境下,有可能會出現髒讀(Dirty Read)、不可重復讀(Unrepeatable Read)、 幻讀(Phantom Read)、更新丟失(Lost update)等情況,所以mysql引入了很多鎖的概念
MySQL InnoDB對數據行的鎖定類型一共有四種:共享鎖(讀鎖,S鎖)、排他鎖(寫鎖,X鎖)、意向共享鎖(IS鎖)和意向排他鎖(IX鎖),支持三種行鎖定方式:
默認情況下,InnoDB工作在可重復讀隔離級別下,並且以Next-Key Lock的方式對數據行進行加鎖,這樣可以有效防止幻讀的發生。Next-Key Lock是行鎖與間隙鎖的組合,這樣,當InnoDB掃描索引記錄的時候,會首先對選中的索引記錄加上行鎖(Record Lock),再對索引記錄兩邊的間隙加上間隙鎖(Gap Lock)。如果一個間隙被事務T1加了鎖,其它事務是不能在這個間隙插入記錄的。
悲觀鎖是指對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個數據處理過程中,將數據處於鎖定狀態,在悲觀鎖的情況下,為了保證事務的隔離性,就需要一致性鎖定讀。讀取數據時給加鎖,其它事務無法修改這些數據。修改刪除數據時也要加鎖,其它事務無法讀取這些數據。
悲觀鎖(pessimistic locking)體現了一種謹慎的處事態度。其流程如下:
悲觀鎖確實很嚴謹,有效保證了數據的一致性,在C/S應用上有諸多成熟方案。 但是他的缺點與優點一樣的明顯
MySQL 采用 autocommit 模式運行。這意味著,當執行一個用於更新(修改)表的語句之後,MySQL立刻把更新到buffer中,同時記錄鎖也會被釋放。因此如果事務要執行多條更新(修改)語句,那麼從第2條更新語句開始就是在無鎖條件下執行了,這樣會導致事務失效,破壞數據一致性
mysql> select @@autocommit; +--------------+ | @@autocommit | +--------------+ | 1 | +--------------+
關閉自動提交
set autocommit=0; or [mysqld] init_connect='SET autocommit=0' ;
避免此問題的方法就是關閉 autocommit,然後通過執行 commit 語句來提交事務
$db->begin(); $db->query("SET autocommit=0"); ... ... $db->commit();
注意:
1、不能將"關閉autocommit"作為缺省設置,否則在 innodb 表上執行的查詢操作也將因為沒有執行 commit 或者 rollback 而一直鎖表!因此只能在需要時局部關閉 autocommit,並在操作完成後開啟 autocommit
2、連接mysql用戶的權限不能大於啟動mysql的用戶的權限,不然init_connect='SET autocommit=0'根本不會啟作用,也不會報任何錯誤
If a user has SUPER privilege, init_connect will not execute,(otherwise if init_connect will a wrong query no one can connect to server).
Note, if init_connect is a wrong query, the connection is closing without any errors and next command will clause 'lost connection' error.
要使用悲觀鎖,我們必須關閉mysql數據庫的自動提交屬性,因為MySQL默認使用autocommit模式,也就是說,當你執行一個更新操作後,MySQL會立刻將結果進行提交
mysql> set autocommit = 0; Query OK, 0 rows affected (0.01 sec) mysql> select @@autocommit; +--------------+ | @@autocommit | +--------------+ | 0 | +--------------+ 1 row in set (0.00 sec)
Connect a:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select age from users where id =1 for update; +-----+ | age | +-----+ | 24 | +-----+ 1 row in set (0.00 sec) mysql> update users set age = 25 where id =1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> commit; Query OK, 0 rows affected (0.01 sec)
Connect b: 在a長時間未提交的時候
mysql> select * from users where id =1 for update; ERROR 1205 : Lock wait timeout exceeded; try restarting transaction mysql> update users set age = 22 where id =1; ERROR 1205 : Lock wait timeout exceeded; try restarting transaction
connect b 只有在 connect a 提交之後才會執行,否則會一直等待
在事務中,只有SELECT ... FOR UPDATE 或LOCK IN SHARE MODE 會等待其它事務結束後才執行,一般SELECT ... 則不受此影響
上面我們提到,使用select…for update會把數據給鎖住,不過我們需要注意一些鎖的級別,MySQL InnoDB默認Row-Level Lock,所以只有「明確」地指定主鍵/索引,MySQL 才會執行Row lock (只鎖住被選取的數據) ,否則MySQL 將會執行Table Lock (將整個數據表單給鎖住)
如果一個條件無法通過索引快速過濾,存儲引擎層面就會將所有記錄加鎖後返回,再由MySQL Server層進行過濾,但在實際使用過程當中,MySQL做了一些改進,在MySQL Server過濾條件,發現不滿足後,會調用unlock_row方法,把不滿足條件的記錄釋放鎖 (違背了二段鎖協議的約束)。這樣做,保證了最後只會持有滿足條件記錄上的鎖,但是每條記錄的加鎖操作還是不能省略的。可見即使是MySQL,為了效率也是會違反規范的
這種情況同樣適用於MySQL的默認隔離級別RR。所以對一個數據量很大的表做批量修改的時候,如果無法使用相應的索引,MySQL Server過濾數據的的時候特別慢,就會出現雖然沒有修改某些行的數據,但是它們還是被鎖住了的現象
悲觀並發控制實際上是“先取鎖再訪問”的保守策略,為數據處理的安全提供了保證。但是在效率方面,處理加鎖的機制會讓數據庫產生額外的開銷,還有增加產生死鎖的機會;
另外,在只讀型事務處理中由於不會產生沖突,也沒必要使用鎖,這樣做只能增加系統負載;還有會降低了並行性,一個事務如果鎖定了某行數據,其他事務就必須等待該事務處理完才可以處理那行數據
悲觀的缺陷是不論是頁鎖還是行鎖,加鎖的時間可能會很長,這樣可能會長時間的限制其他用戶的訪問,也就是說悲觀鎖的並發訪問性不好
樂觀鎖則認為其他用戶企圖改變你正在更改的對象的概率是很小的,因此樂觀鎖直到你准備提交所作的更改時才將對象鎖住,當你讀取以及改變該對象時並不加鎖。可見樂觀鎖加鎖的時間要比悲觀鎖短,樂觀鎖可以用較大的鎖粒度獲得較好的並發訪問性能
參考文章
http://tech.meituan.com/innodb-lock.html
http://hedengcheng.com/?p=771
http://ouyanggod.iteye.com/blog/2166384