鎖是計算機協調多個進程或純線程並發訪問某一資源的機制。在數據庫中,除傳統的計算資源(CPU、RAM、I/O)的爭用以外,數據也是一種供許多用戶共享的資源。如何保證數據並發訪問的一致性、有效性是所在有數據庫必須解決的一個問題,鎖沖突也是影響數據庫並發訪問性能的一個重要因素。從這個角度來說,鎖對數據庫而言顯得尤其重要,也更加復雜。
概述
- 表級鎖:開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖沖突的概率最高,並發度最低。
- 行級鎖:開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖沖突的概率最低,並發度也最高。
- 頁面鎖:開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表鎖和行鎖之間,並發度一般

MySQL表級鎖的鎖模式(MyISAM)
- 對MyISAM的讀操作,不會阻塞其他用戶對同一表請求,但會阻塞對同一表的寫請求;
- 對MyISAM的寫操作,則會阻塞其他用戶對同一表的讀和寫操作;
- MyISAM表的讀操作和寫操作之間,以及寫操作之間是串行的。
MySQL表級鎖的鎖模式
MySQL中的表鎖兼容性
當前鎖模式/是否兼容/請求鎖模式 |
讀鎖 |
寫鎖 |
讀鎖 | 是 | 否 |
寫鎖 | 否 | 否 |
如何加表鎖
1
2
|
SELECT
SUM
(total)
FROM
orders;
SELECT
SUM
(subtotal)
FROM
order_detail;
|
1
2
3
4
|
LOCK tables orders
read
local
,order_detail
read
local
;
SELECT
SUM
(total)
FROM
orders;
SELECT
SUM
(subtotal)
FROM
order_detail;
Unlock tables;
|
- 上面的例子在LOCK TABLES時加了‘local’選項,其作用就是在滿足MyISAM表並發插入條件的情況下,允許其他用戶在表尾插入記錄
- 在用LOCKTABLES給表顯式加表鎖是時,必須同時取得所有涉及表的鎖,並且MySQL支持鎖升級。也就是說,在執行LOCK TABLES后,只能訪問顯式加鎖的這些表,不能訪問未加鎖的表;同時,如果加的是讀鎖,那么只能執行查詢操作,而不能執行更新操作。其實,在自動加鎖的情況下也基本如此,MySQL問題一次獲得SQL語句所需要的全部鎖。這也正是MyISAM表不會出現死鎖(Deadlock Free)的原因
並發鎖
concurrent_insert和local操作(此操作為MyISAM引擎專有,InnoDB無此功能)
上面我們說到只要給一個表加了讀鎖,其他session對該表的寫操作將被阻塞。那么有沒有辦法讓其他session也能往里面添加數據呢?
這里我們可以使用local關鍵字,語法如下:lock table 表名 read local。這樣在當前表被加讀鎖的時候,可以讓其他session往表里添加記錄,但需要配合concurrent_insert全局變量使用。
- 當concurrent_insert設置為0時,不允許並發插入。
- 當concurrent_insert設置為1時,如果MyISAM允許在一個讀表的同時,另一個進程從表尾插入記錄。這也是MySQL的默認設置。如果MyISAM表中沒有空洞洞(即表的中間沒有被刪除的⾏行行),MyISAM允許在一個進程讀表的同時,另一個進程從表尾插⼊入記錄。這也是MySQL的默認設置。
- 當concurrent_insert設置為2時,無論MyISAM表中有沒有空洞,都允許在表尾插入記錄,都允許在表尾並發插入記錄。
MyISAM的鎖調度
- 通過指定啟動參數low-priority-updates,使MyISAM引擎默認給予讀請求以優先的權利。
- 通過執行命令SET LOW_PRIORITY_UPDATES=1,使該連接發出的更新請求優先級降低。
- 通過指定INSERT、UPDATE、DELETE語句的LOW_PRIORITY屬性,降低該語句的優先級。
InnoDB鎖問題
1.事務(Transaction)及其ACID屬性
- 原性性(Actomicity):事務是一個原子操作單元,其對數據的修改,要么全都執行,要么全都不執行。
- 一致性(Consistent):在事務開始和完成時,數據都必須保持一致狀態。這意味着所有相關的數據規則都必須應用於事務的修改,以操持完整性;事務結束時,所有的內部數據結構(如B樹索引或雙向鏈表)也都必須是正確的。
- 隔離性(Isolation):數據庫系統提供一定的隔離機制,保證事務在不受外部並發操作影響的“獨立”環境執行。這意味着事務處理過程中的中間狀態對外部是不可見的,反之亦然。
- 持久性(Durable):事務完成之后,它對於數據的修改是永久性的,即使出現系統故障也能夠保持。
2.並發事務帶來的問題
- 更新丟失(Lost Update):當兩個或多個事務選擇同一行,然后基於最初選定的值更新該行時,由於每個事務都不知道其他事務的存在,就會發生丟失更新問題——最后的更新覆蓋了其他事務所做的更新。例如,兩個編輯人員制作了同一文檔的電子副本。每個編輯人員獨立地更改其副本,然后保存更改后的副本,這樣就覆蓋了原始文檔。最后保存其更改保存其更改副本的編輯人員覆蓋另一個編輯人員所做的修改。如果在一個編輯人員完成並提交事務之前,另一個編輯人員不能訪問同一文件,則可避免此問題
- 臟讀(Dirty Reads):A事務讀取B事務尚未提交的更改數據,並在這個數據的基礎上進行操作,這時候如果事務B回滾,那么A事務讀到的數據是不被承認的。。這種現象被形象地叫做“臟讀”。
- 不可重復讀(Non-Repeatable Reads):事務A首先讀取了一條數據,然后執行邏輯的時候,事務B將這條數據改變了,然后事務A再次讀取的時候,發現數據不匹配了,就是所謂的不可重復讀了。
- 也就是說,當前事務先進行了一次數據讀取,然后再次讀取到的數據是別的事務修改成功的數據,導致兩次讀取到的數據不匹配,也就照應了不可重復讀的語義。
- 幻讀(Phantom Reads):事務A首先根據條件索引得到N條數據,然后事務B改變了這N條數據之外的M條或者增添了M條符合事務A搜索條件的數據,導致事務A再次搜索發現有N+M條數據了,就產生了幻讀。
也就是說,當前事務讀第一次取到的數據比后來讀取到數據條目少。
不可重復讀和幻讀比較:
兩者有些相似,但是前者針對的是update或delete,后者針對的insert。
注意:不可重復讀和幻讀的區別是:前者是指讀到了已經提交的事務的更改數據(修改或刪除),后者是指讀到了其他已經提交事務的新增數據。
3.事務隔離級別
事務4種隔離級別比較
隔離級別/讀數據一致性及允許的並發副作用 | 讀數據一致性 | 臟讀 | 不可重復讀 | 幻讀 |
未提交讀(Read uncommitted)
|
最低級別,只能保證不讀取物理上損壞的數據 | 是 | 是 | 是 |
已提交度(Read committed) | 語句級 | 否 | 是 | 是 |
可重復讀(Repeatable read) | 事務級 | 否 | 否 | 是 |
可序列化(Serializable) | 最高級別,事務級 | 否 | 否 | 否 |
獲取InonoD行鎖爭用情況
1
2
3
4
5
6
7
8
9
10
11
|
mysql> show status
like
'innodb_row_lock%'
;
+
-------------------------------+-------+
| Variable_name | Value |
+
-------------------------------+-------+
| Innodb_row_lock_current_waits | 0 |
| Innodb_row_lock_time | 0 |
| Innodb_row_lock_time_avg | 0 |
| Innodb_row_lock_time_max | 0 |
| Innodb_row_lock_waits | 0 |
+
-------------------------------+-------+
5
rows
in
set
(0.00 sec)
|
InnoDB的行鎖模式及加鎖方法
- 共享鎖(s):共享鎖又稱為讀鎖,簡稱S鎖,顧名思義,共享鎖就是多個事務對於同一數據可以共享一把鎖,都能訪問到數據,但是只能讀不能修改。
- 排他鎖(X):排他鎖又稱為寫鎖,簡稱X鎖,顧名思義,排他鎖就是不能與其他所並存,如一個事務獲取了一個數據行的排他鎖,其他事務就不能再獲取該行的其他鎖,包括共享鎖和排他鎖,但是獲取排他鎖的事務是可以對數據就行讀取和修改。

InnoDB行鎖模式兼容性列表
我們有如下測試數據
現在我們對id=1的數據行排他查詢,這里會使用begin開啟事務,而不會看見我關閉事務,這樣做是用來測試,因為提交事務或回滾事務就會釋放鎖。
打開一個查詢窗口
會查詢到一條數據,現在打開另一個查詢窗口,對同一數據分別使用排他查和共享鎖查詢兩種方式查詢
排他查
共享查
我們看到開了排他鎖查詢和共享鎖查詢都會處於阻塞狀態,因為id=1的數據已經被加上了排他鎖,此處阻塞是等待排他鎖釋放。
如果我們直接使用以下查詢呢
我們看到是可以查詢到數據的。
我們再看一下一個事務獲取了共享鎖,在其他查詢中也只能加共享鎖或不加鎖。
我們看到是可以查詢數據的,但加排他鎖就查不到,因為排他鎖與共享鎖不能存在同一數據上。
最后我們驗證下上面說的mysql InnoDb引擎中update,delete,insert語句自動加排他鎖的問題,
此時共享查詢處於阻塞,等待排它鎖的釋放,但是用普通查詢能查到數據,因為沒用上鎖機制不與排他鎖互斥,但查到的數據是修改數據之前的老數據。
然后我們提交數據,釋放排他鎖看下修改后的數據,此時可用排他查,共享查和普通查詢, 因為事務提交后該行數據釋放排他鎖,下面就只顯示普通查詢,其他的同學們自己去驗證。
可以看到結果與預期的一樣。
InnoDB行鎖實現方式

間隙鎖(Next-Key鎖)
SELECT * FROM emp WHERE empid > 100 FOR UPDATE
什么時候使用表鎖
- 第一種情況是:事務需要更新大部分或全部數據,表又比較大,如果使用默認的行鎖,不僅這個事務執行效率低,而且可能造成其他事務長時間鎖等待和鎖沖突,這種情況下可以考慮使用表鎖來提高該事務的執行速度。
- 第二種情況是:事務涉及多個表,比較復雜,很可能引起死鎖,造成大量事務回滾。這種情況也可以考慮一次性鎖定事務涉及的表,從而避免死鎖、減少數據庫因事務回滾帶來的開銷。
1
2
3
4
5
|
SET
AUTOCOMMIT=0;
LOCAK TABLES t1 WRITE, t2
READ
, ...;
[do something
with
tables t1
and
here];
COMMIT
;
UNLOCK TABLES;
|
關於死鎖
總結
- 盡量使用較低的隔離級別
- 精心設計索引,並盡量使用索引訪問數據,使加鎖更精確,從而減少鎖沖突的機會。
- 選擇合理的事務大小,小事務發生鎖沖突的幾率也更小。
- 給記錄集顯示加鎖時,最好一次性請求足夠級別的鎖。比如要修改數據的話,最好直接申請排他鎖,而不是先申請共享鎖,修改時再請求排他鎖,這樣容易產生死鎖。
- 不同的程序訪問一組表時,應盡量約定以相同的順序訪問各表,對一個表而言,盡可能以固定的順序存取表中的行。這樣可以大減少死鎖的機會。
- 盡量用相等條件訪問數據,這樣可以避免間隙鎖對並發插入的影響。
- 不要申請超過實際需要的鎖級別;除非必須,查詢時不要顯示加鎖。
- 對於一些特定的事務,可以使用表鎖來提高處理速度或減少死鎖的可能。