mysql-行锁的实现
一、背景知識
事務(Transaction)及其ACID屬性
事務是由一組SQL語句組成的邏輯處理單元,事務具有以下4個屬性,通常簡稱為事務的ACID屬性。
- 原子性(Atomicity):事務是一個原子操作單元,其對數據的修改,要么全都執行,要么全都不執行。
- 一致性(Consistent):在事務開始和完成時,數據都必須保持一致狀態。這意味著所有相關的數據規則都必須應用于事務的修改,以保持數據的完整性;事務結束時,所有的內部數據結構(如B樹索引或雙向鏈表)也都必須是正確的。
- 隔離性(Isolation):數據庫系統提供一定的隔離機制,保證事務在不受外部并發操作影響的“獨立”環境執行。這意味著事務處理過程中的中間狀態對外部是不可見的,反之亦然。
- 持久性(Durable):事務完成之后,它對于數據的修改是永久性的,即使出現系統故障也能夠保持。
并發事務處理帶來的問題
相對于串行處理來說,并發事務處理能大大增加數據庫資源的利用率,提高數據庫系統的事務吞吐量,從而可以支持更多的用戶。但并發事務處理也會帶來一些問題,主要包括以下幾種情況。
- 更新丟失(Lost Update):當兩個或多個事務選擇同一行,然后基于最初選定的值更新該行時,由于每個事務都不知道其他事務的存在,就會發生丟失更新問題--最后的更新覆蓋了由其他事務所做的更新。例如,兩個編輯人員制作了同一文檔的電子副本。每個編輯人員獨立地更改其副本,然后保存更改后的副本,這樣就覆蓋了原始文檔。最后保存其更改副本的編輯人員覆蓋另一個編輯人員所做的更改。如果在一個編輯人員完成并提交事務之前,另一個編輯人員不能訪問同一文件,則可避免此問題。
- 臟讀(Dirty Reads):一個事務正在對一條記錄做修改,在這個事務完成并提交前,這條記錄的數據就處于不一致狀態;這時,另一個事務也來讀取同一條記錄,如果不加控制,第二個事務讀取了這些“臟”數據,并據此做進一步的處理,就會產生未提交的數據依賴關系。這種現象被形象地叫做”臟讀”。
- 不可重復讀(Non-Repeatable Reads):一個事務在讀取某些數據后的某個時間,再次讀取以前讀過的數據,卻發現其讀出的數據已經發生了改變、或某些記錄已經被刪除了!這種現象就叫做“不可重復讀”。
- 幻讀(Phantom Reads):一個事務按相同的查詢條件重新讀取以前檢索過的數據,卻發現其他事務插入了滿足其查詢條件的新數據,這種現象就稱為“幻讀”。
事務隔離級別
在上面講到的并發事務處理帶來的問題中,“更新丟失”通常是應該完全避免的。但防止更新丟失,并不能單靠數據庫事務控制器來解決,需要應用程序對要更新的數據加必要的鎖來解決,因此,防止更新丟失應該是應用的責任。
“臟讀”、“不可重復讀”和“幻讀”,其實都是數據庫讀一致性問題,必須由數據庫提供一定的事務隔離機制來解決。數據庫實現事務隔離的方式,基本上可分為以下兩種。
- 一種是在讀取數據前,對其加鎖,阻止其他事務對數據進行修改。
- 另一種是不用加任何鎖,通過一定機制生成一個數據請求時間點的一致性數據快照(Snapshot),并用這個快照來提供一定級別(語句級或事務級)的一致性讀取。從用戶的角度來看,好像是數據庫可以提供同一數據的多個版本,因此,這種技術叫做數據多版本并發控制(MultiVersion Concurrency Control,簡稱MVCC或MCC),也經常稱為多版本數據庫。
數據庫的事務隔離越嚴格,并發副作用越小,但付出的代價也就越大,因為事務隔離實質上就是使事務在一定程度上 “串行化”進行,這顯然與“并發”是矛盾的。同時,不同的應用對讀一致性和事務隔離程度的要求也是不同的,比如許多應用對“不可重復讀”和“幻讀”并不敏感,可能更關心數據并發訪問的能力。
為了解決“隔離”與“并發”的矛盾,ISO/ANSI SQL92定義了4個事務隔離級別,每個級別的隔離程度不同,允許出現的副作用也不同,應用可以根據自己的業務邏輯要求,通過選擇不同的隔離級別來平衡 “隔離”與“并發”的矛盾。表20-5很好地概括了這4個隔離級別的特性。
| 未提交讀(Read uncommitted) | 最低級別,只能保證不讀取物理上損壞的數據,事務可以看到其他事務沒有被提交的數據(臟數據) | 是 | 是 | 是 |
| 已提交度(Read committed) | 語句級,事務可以看到其他事務已經提交的數據 | 否 | 是 | 是 |
| 可重復讀(Repeatable read) | 事務級,事務中兩次查詢的結果相同 | 否 | 否 | 是 |
| 可序列化(Serializable) | 串行 | 否 | 否 | 否 |
最后要說明的是:各具體數據庫并不一定完全實現了上述4個隔離級別,例如,
- Oracle只提供Read committed和Serializable兩個標準隔離級別,另外還提供自己定義的Read only隔離級別;
- SQL Server除支持上述ISO/ANSI SQL92定義的4個隔離級別外,還支持一個叫做“快照”的隔離級別,但嚴格來說它是一個用MVCC實現的Serializable隔離級別。
- MySQL 支持全部4個隔離級別,但在具體實現時,有一些特點,比如在一些隔離級別下是采用MVCC一致性讀,但某些情況下又不是,這些內容在后面的章節中將會做進一步介紹。
innodb行鎖實現方式
InnoDB行鎖是通過給索引上的索引項加鎖來實現的,這一點MySQL與Oracle不同,后者是通過在數據塊中對相應數據行加鎖來實現的。?InnoDB這種行鎖實現特點意味著:只有通過索引條件檢索數據,InnoDB才使用行級鎖,否則,InnoDB將使用表鎖!
在實際應用中,要特別注意InnoDB行鎖的這一特性,不然的話,可能導致大量的鎖沖突,從而影響并發性能。下面通過一些實際例子來加以說明.
1、在不通過索引條件查詢的時候,InnoDB確實使用的是表鎖,而不是行鎖
| 1 2 3 4 5 | mysql> create table tab_no_index(id int,name varchar(10)) engine=innodb; Query OK, 0 rows affected (0.15 sec) mysql> insert into tab_no_index values(1,'1'),(2,'2'),(3,'3'),(4,'4'); Query OK, 4 rows affected (0.00 sec) Records: 4? Duplicates: 0? Warnings: 0 |
1)InnoDB存儲引擎的表在不使用索引時使用表鎖例子
| session1 | session2 |
| set autocommit=0; Query OK, 0 rows affected (0.00 sec) mysql> select?from tab_no_index where id = 1 ; +——+——+ | id | name | +——+——+ | 1 | 1 | +——+——+ 1 row in set (0.00 sec) | mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) mysql> select from tab_no_index where id = 2 ; +——+——+ | id | name | +——+——+ | 2 | 2 | +——+——+ 1 row in set (0.00 sec) |
| mysql> select?from tab_no_index where id = 1 for update; +——+——+ | id | name | +——+——+ | 1 | 1 | +——+——+ 1 row in set (0.00 sec) | ? |
| ? | mysql> select from tab_no_index where id = 2 for update; 等待 |
在上如表中,看起來session_1只給一行加了排他鎖,但session_2在請求其他行的排他鎖時,卻出現了鎖等待!原因就是在沒有索引的情況下,InnoDB只能使用表鎖。
2)有了索引以后,在對索引字段查詢時,使用的就是行級鎖:
添加索引:alter table tab_no_index add index?ind_tab_no_index_id (id);
| session1 | session2 |
| mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) mysql> select?from tab_no_index where id = 1 ; +——+——+ | id | name | +——+——+ | 1 | 1 | +——+——+ 1 row in set (0.00 sec) | mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) mysql> select from tab_no_index where id = 2 ; +——+——+ | id | name | +——+——+ | 2 | 2 | +——+——+ 1 row in set (0.00 sec) |
| mysql> select?from tab_no_index where id = 1 for update; +——+——+ | id | name | +——+——+ | 1 | 1 | +——+——+ 1 row in set (0.00 sec) | ? |
| ? | mysql> select from tab_no_index where id = 2 for update; +——+——+ | id | name | +——+——+ | 2 | 2 | +——+——+ |
2、由于MySQL的行鎖是針對索引加的鎖,不是針對記錄加的鎖,所以雖然是訪問不同行的記錄,但是如果是使用相同的索引鍵,是會出現鎖沖突的。應用設計的時候要注意這一點。
表tab_with_index的id字段有索引,name字段沒有索引。
| 1 2 3 4 5 6 7 | mysql> select * from tab_with_index where id = 1; +------+------+ | id | name | +------+------+ | 1 | 1 | | 1 | 4 | +------+------+ |
InnoDB存儲引擎使用相同索引鍵的阻塞例子
| session1 | session2 |
| mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) | mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) |
| mysql> select?from tab_with_index where id = 1 and name = ‘1’ for update; +——+——+ | id | name | +——+——+ | 1 | 1 | +——+——+ 1 row in set (0.00 sec) | ? |
| ? | 雖然session_2訪問的是和session_1不同的記錄,但是因為使用了相同的索引,所以需要等待鎖: mysql> select from tab_with_index where id = 1 and name = ‘4’ for update; 等待 |
3、當表有多個索引的時候,不同的事務可以使用不同的索引鎖定不同的行,另外,不論是使用主鍵索引、唯一索引或普通索引,InnoDB都會使用行鎖來對數據加鎖。
表tab_with_index的id字段有普通索引,name字段有普通索引:
InnoDB存儲引擎的表使用不同索引的阻塞例子
| session1 | session2 |
| mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) | mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) |
| mysql> select?from tab_with_index where id = 1 for update; +——+——+ | id | name | +——+——+ | 1 | 1 | | 1 | 4 | +——+——+ 2 rows in set (0.00 sec) | ? |
| ? | Session_2使用name的索引訪問記錄,因為記錄沒有被索引,所以可以獲得鎖: mysql> select from tab_with_index where name = ‘2’ for update; +——+——+ | id | name | +——+——+ | 2 | 2 | +——+——+ 1 row in set (0.00 sec) |
| ? | 由于訪問的記錄已經被session_1鎖定,所以等待獲得鎖。: mysql> select * from tab_with_index where name = ‘4’ for update; |
4、間隙鎖(Next-Key鎖)
當我們用范圍條件而不是相等條件檢索數據,并請求共享或排他鎖時,InnoDB會給符合條件的已有數據記錄的索引項加鎖;對于鍵值在條件范圍內但并不存在的記錄,叫做“間隙(GAP)”,InnoDB也會對這個“間隙”加鎖,這種鎖機制就是所謂的間隙鎖(Next-Key鎖)。
舉例來說,假如emp表中只有101條記錄,其empid的值分別是 1,2,…,100,101,下面的SQL:
Select * from emp where empid > 100 for update;
是一個范圍條件的檢索,InnoDB不僅會對符合條件的empid值為101的記錄加鎖,也會對empid大于101(這些記錄并不存在)的“間隙”加鎖。
InnoDB使用間隙鎖的目的,一方面是為了防止幻讀,以滿足相關隔離級別的要求,對于上面的例子,要是不使用間隙鎖,如果其他事務插入了empid大于100的任何記錄,那么本事務如果再次執行上述語句,就會發生幻讀;另外一方面,是為了滿足其恢復和復制的需要。有關其恢復和復制對鎖機制的影響,以及不同隔離級別下InnoDB使用間隙鎖的情況,在后續的章節中會做進一步介紹。
很顯然,在使用范圍條件檢索并鎖定記錄時,InnoDB這種加鎖機制會阻塞符合條件范圍內鍵值的并發插入,這往往會造成嚴重的鎖等待。因此,在實際應用開發中,尤其是并發插入比較多的應用,我們要盡量優化業務邏輯,盡量使用相等條件來訪問更新數據,避免使用范圍條件。
還要特別說明的是,InnoDB除了通過范圍條件加鎖時使用間隙鎖外,如果使用相等條件請求給一個不存在的記錄加鎖,InnoDB也會使用間隙鎖!
下面例子中,假如emp表中只有101條記錄,其empid的值分別是1,2,……,100,101。
InnoDB存儲引擎的間隙鎖阻塞例子
| session1 | session1 |
| mysql> select @@tx_isolation; +—————–+ | @@tx_isolation | +—————–+ | REPEATABLE-READ | +—————–+ 1 row in set (0.00 sec) mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) | mysql> select @@tx_isolation; +—————–+ | @@tx_isolation | +—————–+ | REPEATABLE-READ | +—————–+ 1 row in set (0.00 sec) mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) |
| 當前session對不存在的記錄加for update的鎖: mysql> select * from emp where empid = 102 for update; Empty set (0.00 sec) | ? |
| ? | 這時,如果其他session插入empid為102的記錄(注意:這條記錄并不存在),也會出現鎖等待: mysql>insert into emp(empid,…) values(102,…); 阻塞等待 |
| Session_1 執行rollback: mysql> rollback; Query OK, 0 rows affected (13.04 sec) | ? |
| ? | 由于其他session_1回退后釋放了Next-Key鎖,當前session可以獲得鎖并成功插入記錄: mysql>insert into emp(empid,…) values(102,…); Query OK, 1 row affected (13.35 sec) |
5、即便在條件中使用了索引字段,但是否使用索引來檢索數據是由MySQL通過判斷不同執行計劃的代價來決定的,如果MySQL認為全表掃描效率更高,比如對一些很小的表,它就不會使用索引,這種情況下InnoDB將使用表鎖,而不是行鎖。因此,在分析鎖沖突時,別忘了檢查SQL的執行計劃,以確認是否真正使用了索引。
來源:http://blog.csdn.net/alexdamiao/article/details/52049993
總結
以上是生活随笔為你收集整理的mysql-行锁的实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 农行微信预约纪念币的流程
- 下一篇: linux cmake编译源码,linu