sql server 鎖(lock)的基礎及應用
一 關於鎖的基礎知識
(一). 為什麼要引入鎖
當多個使用者同時對資料庫的併發操作時會帶來以下資料不一致的問題:
◆丟失更新
A,B兩個使用者讀同一資料並進行修改,其中一個使用者的修改結果破壞了另一個修改的結果,比如訂票系統
◆髒讀
A使用者修改了資料,隨後B使用者又讀出該資料,但A使用者因為某些原因取消了對資料的修改,資料恢復原值,此時B得到的資料就與資料庫內的資料產生了不一致
◆不可重複讀
A使用者讀取資料,隨後B使用者讀出該資料並修改,此時A使用者再讀取資料時發現前後兩次的值不一致
併發控制的主要方法是封鎖,鎖就是在一段時間內禁止使用者做某些操作以避免產生資料不一致
(二) 鎖的分類
◆鎖的類別有兩種分法:
1. 從資料庫系統的角度來看:分為獨佔鎖(即排它鎖),共享鎖和更新鎖
MS-SQL Server 使用以下資源鎖模式。
鎖模式 描述
共享 (S) 用於不更改或不更新資料的操作(只讀操作),如 SELECT 語句。
更新 (U) 用於可更新的資源中。防止當多個會話在讀取、鎖定以及隨後可能進行的資源更新時發生常見形式的死鎖。
排它 (X) 用於資料修改操作,例如 INSERT、UPDATE 或 DELETE。確保不會同時同一資源進行多重更新。
意向鎖 用於建立鎖的層次結構。意向鎖的型別為:意向共享 (IS)、意向排它 (IX) 以及與意向排它共享 (SIX)。
架構鎖 在執行依賴於表架構的操作時使用。架構鎖的型別為:架構修改 (Sch-M) 和架構穩定性 (Sch-S)。
大容量更新 (BU) 向表中大容量複製資料並指定了 TABLOCK 提示時使用。
◆共享鎖
共享 (S) 鎖允許併發事務讀取 (SELECT) 一個資源。資源上存在共享 (S) 鎖時,任何其它事務都不能修改資料。一旦已經讀取資料,便立即釋放資源上的共享 (S) 鎖,除非將事務隔離級別設定為可重複讀或更高級別,或者在事務生存週期內用鎖定提示保留共享 (S) 鎖。
◆更新鎖
更新 (U) 鎖可以防止通常形式的死鎖。一般更新模式由一個事務組成,此事務讀取記錄,獲取資源(頁或行)的共享 (S) 鎖,然後修改行,此操作要求鎖轉換為排它 (X) 鎖。如果兩個事務獲得了資源上的共享模式鎖,然後試圖同時更新資料,則一個事務嘗試將鎖轉換為排它 (X) 鎖。共享模式到排它鎖的轉換必須等待一段時間,因為一個事務的排它鎖與其它事務的共享模式鎖不相容;發生鎖等待。第二個事務試圖獲取排它 (X) 鎖以進行更新。由於兩個事務都要轉換為排它 (X) 鎖,並且每個事務都等待另一個事務釋放共享模式鎖,因此發生死鎖。
若要避免這種潛在的死鎖問題,請使用更新 (U) 鎖。一次只有一個事務可以獲得資源的更新 (U) 鎖。如果事務修改資源,則更新 (U) 鎖轉換為排它 (X) 鎖。否則,鎖轉換為共享鎖。
◆排它鎖
排它 (X) 鎖可以防止併發事務對資源進行訪問。其它事務不能讀取或修改排它 (X) 鎖鎖定的資料
◆意向鎖
意向鎖表示 SQL Server 需要在層次結構中的某些底層資源上獲取共享 (S) 鎖或排它 (X) 鎖。例如,放置在表級的共享意向鎖表示事務打算在表中的頁或行上放置共享 (S) 鎖。在表級設定意向鎖可防止另一個事務隨後在包含那一頁的表上獲取排它 (X) 鎖。意向鎖可以提高效能,因為 SQL Server 僅在表級檢查意向鎖來確定事務是否可以安全地獲取該表上的鎖。而無須檢查表中的每行或每頁上的鎖以確定事務是否可以鎖定整個表。
意向鎖包括意向共享 (IS)、意向排它 (IX) 以及與意向排它共享 (SIX)。
鎖模式 描述
意向共享 (IS) 通過在各資源上放置 S 鎖,表明事務的意向是讀取層次結構中的部分(而不是全部)底層資源。
意向排它 (IX) 通過在各資源上放置 X 鎖,表明事務的意向是修改層次結構中的部分(而不是全部)底層資源。IX 是 IS 的超集。
與意向排它共享 (SIX) 通過在各資源上放置 IX 鎖,表明事務的意向是讀取層次結構中的全部底層資源並修改部分(而不是全部)底層資源。允許頂層資源上的併發 IS 鎖。例如,表的 SIX 鎖在表上放置一個 SIX 鎖(允許併發 IS 鎖),在當前所修改頁上放置 IX 鎖(在已修改行上放置 X 鎖)。雖然每個資源在一段時間內只能有一個 SIX 鎖,以防止其它事務對資源進行更新,但是其它事務可以通過獲取表級的 IS 鎖來讀取層次結構中的底層資源。
◆獨佔鎖:
只允許進行鎖定操作的程式使用,其他任何對他的操作均不會被接受。執行資料更新命令時,SQL Server會自動使用獨佔鎖。當物件上有其他鎖存在時,無法對其加獨佔鎖。
共享鎖:共享鎖鎖定的資源可以被其他使用者讀取,但其他使用者無法修改它,在執行Select時,SQL Server會對物件加共享鎖。
◆更新鎖:
當SQL Server準備更新資料時,它首先對資料物件作更新鎖鎖定,這樣資料將不能被修改,但可以讀取。等到SQL Server確定要進行更新資料操作時,他會自動將更新鎖換為獨佔鎖,當物件上有其他鎖存在時,無法對其加更新鎖。
2. 從程式設計師的角度看:分為樂觀鎖和悲觀鎖。
◆樂觀鎖:完全依靠資料庫來管理鎖的工作。
◆悲觀鎖:程式設計師自己管理資料或物件上的鎖處理。
MS-SQLSERVER 使用鎖在多個同時在資料庫內執行修改的使用者間實現悲觀併發控制
三 鎖的粒度
鎖粒度是被封鎖目標的大小,封鎖粒度小則併發性高,但開銷大,封鎖粒度大則併發性低但開銷小
SQL Server支援的鎖粒度可以分為為行、頁、鍵、鍵範圍、索引、表或資料庫獲取鎖
資源 描述
RID 行識別符號。用於單獨鎖定表中的一行。
鍵 索引中的行鎖。用於保護可序列事務中的鍵範圍。
頁 8 千位元組 (KB) 的資料頁或索引頁。
擴充套件盤區 相鄰的八個資料頁或索引頁構成的一組。
表 包括所有資料和索引在內的整個表。
DB 資料庫。
SQL Server 提供以下的鎖級別:
DATABASE-- 無論何時當一個SQL Server 程序正在使用除master以外的資料庫時,Lock Manager為該程序授予資料庫級的鎖。資料庫級的鎖總是共享鎖,用於跟蹤何時資料庫在使用中,以防其他程序刪除該資料庫,將資料庫置為離線,或者恢復資料庫。注意,由於master和tempdb資料庫不能被刪除或置為離線,所以不需要在它們之上加鎖。
FILE-- 檔案級的鎖用於鎖定資料庫檔案。
EXTENT-- Extent鎖用於鎖定extents,通常僅在空間分配和重新分配的時候使用。一個extent由8個連續的資料頁或索引頁組成。Extent鎖可以是共享鎖也可以是獨佔鎖。
ALLOCATION_UNIT-- 使用在資料庫分配單元上。
TABLE-- 這種級別的鎖將鎖定整個表,包括資料和索引。何時將獲得表級鎖的例子包括在Serializable隔離級別下從包含大量資料的表中選取所有的行,以及在表上執行不帶過濾條件的update或delete。
Heap or B-Tree (HOBT)-- 用於堆資料頁,或者索引的二叉樹結構。
PAGE-- 使用頁級鎖,由8KB資料或者索引資訊組成的整個頁被鎖定。當需要讀取一頁的所有行或者需要執行頁級別的維護如頁拆分後更新頁指標時,將會獲取頁級鎖。
Row ID (RID) -- 使用RID鎖,頁內的單一行被鎖定。無論何時當提供最大化的資源併發性訪問是有效並且可能時,將獲得RID鎖。
KEY-- SQL Server使用兩種型別的Key鎖。其中一個的使用取決於當前會話的鎖隔離級別。對於運行於Read Committed 或者 Repeatable Read 隔離模式下的事務,SQL Server 鎖定與被訪問的行相關聯的的實際索引key。(如果是表的聚集索引,資料行位於索引的葉級。行上在這些你看到的是Key鎖而不是行級鎖。)若在Serializable隔離模式下,通過鎖定一定範圍的key值從而不允許新的行插入到該範圍內,SQL Server防止了“幻讀”。這些鎖因而被稱作“key-range lock”。
METADATA-- 用於鎖定系統目錄資訊(元資料)。
APPLICATION-- 允許使用者定義他們自己的鎖,指定資源名稱、鎖模式、所有者、timeout間隔。
四 SQL Server 鎖型別(與粒度相對應)
1. HOLDLOCK: 在該表上保持共享鎖,直到整個事務結束,而不是在語句執行完立即釋放所新增的鎖。
2. NOLOCK:不新增共享鎖和排它鎖,當這個選項生效後,可能讀到未提交讀的資料或“髒資料”,這個選項僅僅應用於SELECT語句。
3. PAGLOCK:指定新增頁鎖(否則通常可能新增表鎖)。
4. READCOMMITTED用與執行在提交讀隔離級別的事務相同的鎖語義執行掃描。預設情況下,SQL Server 2000 在此隔離級別上操作。
5. READPAST: 跳過已經加鎖的資料行,這個選項將使事務讀取資料時跳過那些已經被其他事務鎖定的資料行,而不是阻塞直到其他事務釋放鎖,READPAST僅僅應用於READ COMMITTED隔離性級別下事務操作中的SELECT語句操作。
6. READUNCOMMITTED:等同於NOLOCK。
7. REPEATABLEREAD:設定事務為可重複讀隔離性級別。
8. ROWLOCK:使用行級鎖,而不使用粒度更粗的頁級鎖和表級鎖。
9. SERIALIZABLE:用與執行在可序列讀隔離級別的事務相同的鎖語義執行掃描。等同於 HOLDLOCK。
10. TABLOCK:指定使用表級鎖,而不是使用行級或頁面級的鎖,SQL Server在該語句執行完後釋放這個鎖,而如果同時指定了HOLDLOCK,該鎖一直保持到這個事務結束。
11. TABLOCKX:指定在表上使用排它鎖,這個鎖可以阻止其他事務讀或更新這個表的資料,直到這個語句或整個事務結束。
12. UPDLOCK :指定在讀表中資料時設定更新 鎖(update lock)而不是設定共享鎖,該鎖一直保持到這個語句或整個事務結束,使用UPDLOCK的作用是允許使用者先讀取資料(而且不阻塞其他使用者讀資料),並且保證在後來再更新資料時,這一段時間內這些資料沒有被其他使用者修改。
五 鎖定時間的長短
鎖保持的時間長度為保護所請求級別上的資源所需的時間長度。
用於保護讀取操作的共享鎖的保持時間取決於事務隔離級別。採用 READ COMMITTED 的預設事務隔離級別時,只在讀取頁的期間內控制共享鎖。在掃描中,直到在掃描內的下一頁上獲取鎖時才釋放鎖。如果指定 HOLDLOCK 提示或者將事務隔離級別設定為 REPEATABLE READ 或 SERIALIZABLE,則直到事務結束才釋放鎖。
根據為遊標設定的併發選項,遊標可以獲取共享模式的滾動鎖以保護提取。當需要滾動鎖時,直到下一次提取或關閉遊標(以先發生者為準)時才釋放滾動鎖。但是,如果指定 HOLDLOCK,則直到事務結束才釋放滾動鎖。
用於保護更新的排它鎖將直到事務結束才釋放。
如果一個連線試圖獲取一個鎖,而該鎖與另一個連線所控制的鎖衝突,則試圖獲取鎖的連線將一直阻塞到:
將衝突鎖釋放而且連接獲取了所請求的鎖。
連線的超時間隔已到期。預設情況下沒有超時間隔,但是一些應用程式設定超時間隔以防止無限期等待
六 SQL Server 中鎖的自定義
◆處理死鎖和設定死鎖優先順序
死鎖就是多個使用者申請不同封鎖,由於申請者均擁有一部分封鎖權而又等待其他使用者擁有的部分封鎖而引起的無休止的等待
可以使用SET DEADLOCK_PRIORITY控制在發生死鎖情況時會話的反應方式。如果兩個程序都鎖定資料,並且直到其它程序釋放自己的鎖時,每個程序才能釋放自己的鎖,即發生死鎖情況。
◆2 處理超時和設定鎖超時持續時間。
@@LOCK_TIMEOUT 返回當前會話的當前鎖超時設定,單位為毫秒
SET LOCK_TIMEOUT 設定允許應用程式設定語句等待阻塞資源的最長時間。當語句等待的時間大於 LOCK_TIMEOUT 設定時,系統將自動取消阻塞的語句,並給應用程式返回"已超過了鎖請求超時時段"的 1222 號錯誤資訊
示例
下例將鎖超時期限設定為 1,800 毫秒。
SET LOCK_TIMEOUT 1800
◆設定事務隔離級別。
◆對 SELECT、INSERT、UPDATE 和 DELETE 語句使用表級鎖定提示。
◆配置索引的鎖定粒度
可以使用 sp_indexoption 系統儲存過程來設定用於索引的鎖定粒度
七 檢視鎖的資訊
1 執行 EXEC SP_LOCK 報告有關鎖的資訊
2 查詢分析器中按Ctrl+2可以看到鎖的資訊
八 使用注意事項
如何避免死鎖,最小化鎖競爭
1 使用事務時,儘量縮短事務的邏輯處理過程,及早提交或回滾事務,事務持有鎖的時間越短,鎖競爭發生的機會就越少;將不是事務所管理的工作單元鎖必需的命令移出事務。;
2 設定死鎖超時引數為合理範圍,如:3分鐘-10分種;超過時間,自動放棄本次操作,避免程序懸掛;
3 優化程式,檢查並避免死鎖現象出現;
4 .對所有的指令碼和SP都要仔細測試,在正是版本之前。
5 所有的SP都要有錯誤處理(通過@error)
6 一般不要修改SQL SERVER事務的預設級別。不推薦強行加鎖
7將組成事務的語句作為一個的單獨的批命令處理,以消除 BEGIN TRAN 和 COMMIT TRAN 語句之間的網路延遲造成的不必要的延遲。
8 考慮完全地使用儲存過程編寫事務程式碼。典型地,儲存過程比批命令執行更快。
9 在遊標中儘可早地Commit更新。因為遊標處理比面向集合的處理慢得多,因此導致鎖被持有的時間更久。
10 使用每個程序所需的最低級別的鎖隔離。比如說,如果髒讀是可接受的並且不要求結果必須精確,那麼可以考慮使用事務隔離級別0(Read Uncommitted),僅在絕對必要時才使用Repeatable Read or Serializable隔離級別。
11 在 BEGIN TRAN 和 COMMIT TRAN 語句之間,絕不允許使用者互動,因為這樣做可能鎖被持有無限期的時間。
九 幾個有關鎖的問題
1 如何鎖一個表的某一行
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT * FROM table ROWLOCK WHERE id = 1
2 鎖定資料庫的一個表
SELECT * FROM table WITH (HOLDLOCK)
加鎖語句:
sybase:
update 表 set col1=col1 where 1=0 ;
MSSQL:
select col1 from 表 (tablockx) where 1=0 ;
Oracle:
LOCK TABLE 表 IN EXCLUSIVE MODE ;
加鎖後其它人不可操作,直到加鎖使用者解鎖,用commit或rollback解鎖
◆排它鎖
新建兩個連線,在第一個連線中執行以下語句
[sql]view plaincopy- begintran
- updatetable1setA='aa'whereB='b2'
- waitfordelay'00:00:30'--等待30秒
- committran
- --在第二個連線中執行以下語句
- begintran
- select*fromtable1whereB='b2'
- committran
若同時執行上述兩個語句,則select查詢必須等待update執行完畢才能執行即要等待30秒
◆共享鎖
在第一個連線中執行以下語句
[html]view plaincopy- begintran
- select*fromtable1holdlock--holdlock人為加鎖
- whereB='b2'
- waitfordelay'00:00:30'--等待30秒
- committran
◆共享鎖
在第一個連線中執行以下語句
[sql]view plaincopy- begintran
- select*fromtable1holdlock--holdlock人為加鎖
- whereB='b2'
- waitfordelay'00:00:30'--等待30秒
- committran
在第二個連線中執行以下語句
[sql]view plaincopy- begintran
- selectA,Cfromtable1whereB='b2'
- updatetable1setA='aa'whereB='b2'
- committran
若同時執行上述兩個語句,則第二個連線中的select查詢可以執行
而update必須等待第一個事務釋放共享鎖轉為排它鎖後才能執行 即要等待30秒
◆死鎖
[sql]view plaincopy- --在第一個連線中執行以下語句
- begintran
- updatetable1setA='aa'whereB='b2'
- waitfordelay'00:00:30'
- updatetable2setD='d5'whereE='e1'
- committran
- --在第二個連線中執行以下語句
- begintran
- updatetable2setD='d5'whereE='e1'
- waitfordelay'00:00:10'
- updatetable1setA='aa'whereB='b2'
- committran
同時執行,系統會檢測出死鎖,並中止程序
十 應用程式鎖:
應用程式鎖就是客戶端程式碼生成的鎖,而不是sql server本身生成的鎖
處理應用程式鎖的兩個過程
sp_getapplock 鎖定應用程式資源
sp_releaseapplock 為應用程式資源解鎖
注意: 鎖定資料庫的一個表的區別
SELECT * FROM table WITH (HOLDLOCK) 其他事務可以讀取表,但不能更新刪除
SELECT * FROM table WITH (TABLOCKX) 其他事務不能讀取表,更新和刪除
交讀事務使用行版本控制。
使用快照隔離。
使用繫結連線。
二 鎖的分析及應用系列
1 用SqlServer Profile來檢視分析鎖的資訊
這個工具我想大家都明白,它的監視能力真的是無所不能。。。鎖的痙攣狀態也全在它的掌握之中。
1. 首先我做一個Person表,Name欄位設定4000位元組,這樣一個數據頁可以容納2條資料,如下圖:
DROP TABLE dbo.Person CREATE TABLE Person(ID INT IDENTITY,NAME CHAR(4000) DEFAULT 'aaaaa') --插入6條,生成3個數據頁 INSERT INTO dbo.Person DEFAULT VALUES go 6
2. 下面我們看看資料在資料頁的分佈情況。
3. 然後我們開啟Profile,在“事件選擇”的Events中選擇”Lock:Acquired“和”Lock:Released“ ,然後執行,如下圖:
使用測試資料
1. 首先我執行一個簡單的SELECT * FROM dbo.Person,看看錶/資料頁/記錄的加鎖情況。
從圖中可以看到,select執行的大概步驟如下:
第一步:給表(Object)加上IS(意向共享鎖)。
第二步:先給1:78號資料頁加IS鎖,掃描78號資料頁,然後釋放IS鎖。
第三步:同樣的道理掃描之後的資料頁。
第四步:最後釋放表的IS鎖,結束整個鎖流程。
看完上面的一系列的Lock:Acquired 和 Lock:Released的話,你有沒有發現一個問題,不是說好給記錄(RID)加上S鎖麼???這裡沒加,是因為引擎進入78號資料頁的時候,未發現它存在IU鎖或者IX鎖。。。所以。。。這個屬於鎖的組合,後續會說。
2. 接下來用UPDATE dbo.Person SET NAME='bbbbb' WHERE ID=3來看看update的整個過程,乍一看,Profile捕獲到的記錄還是比較多的,下面具體看圖:
第一步: 給表(Object)加上IX鎖,
第二步: 給資料頁(1:78)資料頁分配IU鎖。然後開始逐一掃描78號資料頁的RID記錄,進入前就Acquired,退出後就Released,當掃描完78號資料頁的所有RID後,再釋放78 號資料頁的IU鎖,進入下一個資料頁。。。
第三步: 我們發現ID=3是在89號資料頁上,當引擎掃到該RID之後,我們觀察到89號的資料頁由IU鎖變成了IX鎖,並且把1:89:0(slot為0的記錄)由U鎖變成X鎖,變成X鎖 後,就排斥了其他所有的鎖,這時候就可以進行Update操作了。
第四步: 後面就繼續90號資料頁,步驟類似,第二步和第三步。
不知道細心的你有沒有發現,在Released Object之前我們才釋放1:89:0的X鎖,然後釋放89號資料頁的IX鎖,這說明什麼???說明這個Update是貫穿於這個事務的,不像Select操作中,掃完一個數據頁就釋放一個數據頁。
3. 最後再看一個DELETE FROM dbo.Person WHERE ID=3的操作。
大概掃了一下上面的圖,或許你感覺和Update操作大差不差,會掃描資料頁中的每個記錄並加上U鎖。當在1:89:0槽位中找到了目標記錄後,然後將U鎖轉化為X鎖,具體可以參考Update。
2 深入的探討鎖機制
上一篇我只是做了一個堆表讓大家初步的認識到鎖的痙攣狀態,但是在現實世界上並沒有這麼簡單的事情,起碼我的表不會沒有索引對吧,,,還有就是我的表一定會有很多的連線過來,10:1的讀寫,很多碼農可能都會遇到類似神乎其神的死鎖,卡住,讀不出來,插不進入等等神仙的事情導致效能低下,這篇我們一起來探討下。
一: 當select遇到效能低下的update會怎麼樣?
1. 還是使用原始的person表,插入6條資料,由於是4000位元組,所以兩條資料就是一個數據頁,如下圖:
1 DROP TABLE dbo.Person 2 CREATE TABLE Person(ID INT IDENTITY,NAME CHAR(4000) DEFAULT 'aaaaa') 3 --插入6條資料,剛好3個數據頁 4 INSERT INTO dbo.Person DEFAULT VALUES 5 go 6
2. 為了模擬效能低下的Update操作,我們開個顯式事務來更新ID=4的記錄,並且用profile看一下,如下圖:
1 BEGIN TRAN 2 UPDATE dbo.Person SET NAME='bbbbb' WHERE id=4
3. 然後我們開下另一個會話連線,讀取ID=6的記錄會是怎樣?好奇嗎?
1 SELECT * FROM Person WHERE ID=6
從上面流程你是否看到,當掃描到89號資料頁的slot1槽位的時候卡住了。。。我想你應該知道update正好已經給這條記錄加上了X鎖。。。如果你夠細心,你還會發現,給S鎖附加記錄的條件是在當引擎發現記錄所在的資料頁已經附加上了IX鎖的情況下,才給該號資料頁下的每條記錄附加S鎖,對吧。。。好了,既然在Profile上面看不到了,我還是有其他辦法來判斷到底select語句現在處於什麼狀態。
4. 使用sys.dm_tran_locks來看當前各個連線持有鎖的狀態。
1 SELECT l.request_session_id, 2 DB_NAME(l.resource_database_id),OBJECT_NAME(p.object_id), 3 l.resource_description,l.request_type, 4 l.request_status,request_mode 5 FROM sys.dm_tran_locks AS l 6 LEFT JOIN sys.partitions AS p 7 ON l.resource_associated_entity_id=p.hobt_id
仔細觀察上圖可以看到,當前有51和52號會話,51號在1:89:1槽位上使用了X鎖並且沒有釋放,52號此時也進入了1:89:1中,並且想給該RowID附加S鎖,但是你也知道S和X鎖是排斥的,所以很無奈的一直保持等待狀態。
二:使用索引或許可以幫你逃過一劫
當你看完上面的講敘,是不是有點害怕???要是在生產環境下出現了這種情況,那我們是不是死的很慘???那接下來使用索引是不是真的可以幫我們躲過一劫呢?下面跟我一起看一看。
1. 新建索引index
1 -- 在ID列上建一個index 2 CREATE INDEX idx_person ON dbo.Person(ID)
2. 然後我們看下資料頁的分佈情況,可以看到下圖中78,89,90是表資料頁,93號為索引資料頁。
1 DBCC TRACEON(2588,3604) 2 DBCC IND(Ctrip,Person,-1)
3. 麻蛋的,繼續執行上面的那個慢update
BEGIN TRAN UPDATE dbo.Person SET NAME='bbbbb' WHERE id=4
4. 激動人心的時刻來了,由於資料太少,所以我這裡強制讓引擎執行我建立的索引,看看結果怎樣?
居然沒卡住???現在是不是有一股強烈的好奇心來了,狗狗狗。。。馬上開啟profile,看看到底都發生了什麼?
仔細看完這個圖,是不是覺得很有意思呢???具體步驟如下:
第一步:給表(Object)加上IS鎖。
第二步:因為要走索引,給93號索引資料頁加上IS鎖。
第三步:找到93號索引資料頁的目標key,給這個key加上S鎖,有人可能就會問了。。。這個key不就是6嘛,為什麼這個key=(61005a25560e),你要是太好奇我可以告 訴你,年輕人說話不要太屌,每行索引記錄都有一個雜湊值,這個值就是根據索引的幾個欄位散列出來的,好處就是防止你的索引長度過大,導致鎖這個記錄的 時候太耗費鎖空間了。。。。如果你還是不太相信的話,我用DBCC給你看一看。
第四步:根據這個key直接跳到存放記錄的90號資料頁中,萬幸的是update的記錄剛好不在90號資料頁中。。。。就這樣躲過一劫了。。。然後select順利的讀取到了該 讀的記錄,最後釋放相關的IS鎖。
3 nolock引發的三級事件的一些思考
曾今有件事情讓我記憶猶新,那年剛來攜程不久,馬上就被安排寫一個介面,供企鵝公司呼叫他們員工的差旅資訊,然後我就三下五除二的給寫好了,上線之後,大概過了一個月。。。DBA那邊報告資料庫出現大量鎖超時,並且及時根據sql的來源將email發到了我們部門,指出sql讀取時間過長,並且缺少nolock,影響了大量機票訂單入庫,然後我就拿著sql去生產環境跑了下,22s。。。花擦。。。專案上線時間太久,版本已經不存在了,無法回滾。。。原本準備撤下介面。。。看了下撤下介面跟加上nolock時間相差不多,最後決定先加上nolock,釋出緊急單。。。然後再優化,DBA那邊暫時做手工解鎖,發上去後,最後就是損失XXXX訂單。。。定級為三級事件。然後就是追責,當然這個責任只能有老大們去承擔了,出了這次由我引發的事件,我得思考了,出了事情對我不見得全是壞事,起碼這次會讓我銘記如心,想想也搓,來攜程之前根本就不會關注要不要給select指定nolock,這其中也包括自己沒遇到過大資料吧,也包括自己的能力有限,只知道有鎖這個玩意,細說的話就啥也不知道了,後來才知道攜程有個規則,就是很多業務產線所寫的select都必須指定nolock,懂一點的人可能會說nolock可以提升效能,如果你這樣說,確實是這樣,因為資料庫的鎖是有96位元組開銷的,沒了鎖,也就沒有你在profile中看到accquired和released痙攣了,當你看完我的事件之後,你可能會意識到,效能提升不是最關心的,最關心就是不要出現死鎖,鎖等待。。。好了,言歸正傳,下面我們看看到底在資料庫中可以指定多少個鎖???
一:到底可以指定多少個鎖
這個問題有意思,我們不需要記,只要你裝一個SQL Prompt,有了這個神器,你就知道到底有多少個?如下圖:
1 DROP TABLE dbo.Person 2 CREATE TABLE Person(ID INT IDENTITY,NAME CHAR(4000) DEFAULT 'xxxxx') 3 INSERT INTO dbo.Person DEFAULT VALUES 4 go 6
一眼掃下去,還是蠻多的,不過你要注意了,那些所謂的XXXLock才是我們需要關注的,根據上面的圖,我們大概把鎖分個類。。。
粒度鎖:PAGLOCK,TABLOCK,TABLOCKX,ROWLOCK,NOLOCK
模式鎖:HOLDLOCK, UPDLOCK, XLOCK
接下來我從粒度鎖說起:
1.NOLOCK
都說nolock是無鎖模式的,那到底是怎樣的無鎖呢???到這篇為止,你應該知道,如果不加nolock,我們的表,資料頁是附加IS鎖的,那接下來我用profile看下兩者有什麼區別。
從上圖中,你會看到加上nolock之後,object上面附加了Sch-S鎖,這個鎖叫做“架構穩定鎖”,很簡單就是sql編譯時附加的一把鎖,目的就是防止在編譯時,有其他連線修改表結構,而這個鎖只與Sch-M鎖衝突,與其他鎖都相容,這說明什麼?說明其他連線鎖住了記錄也沒關係,我的nolock不跟他們打交道,這樣的話,就可能會讀到髒資料,不過沒關係,攜程的很多業務是容許髒資料的,畢竟比鎖等待,死鎖要強得多,再說nolock讀到了其他連線未修改或者未提交的資料,這個概率也比較低,就算遇到了也沒關係,一般不會招來客訴的,客人或許再刷下頁面,資料或許就正確了,對不對。。。
2.TABLOCK
這個還是比較見名識義的,就是附加在table上的鎖,也就是表鎖了,很恐怖的。。。下面我舉個Update的例子,看看前後對比。
在上面你有沒有看到,X鎖已經附加到OBJECT上面去了。。。這樣的話,其他連線就動不了這個Object了,只能等待。。。
3. PAGLOCK
看了名字你應該也知道,就是附加到頁面這個級別的鎖,我也舉一個Update的例子。
1 BEGIN TRAN 2 UPDATE dbo.Person SET NAME='aaaaa' WHERE ID=6 3 4 BEGIN TRAN 5 UPDATE dbo.Person WITH(PAGLOCK) SET NAME='bbbbb' WHERE ID=4
從上面兩個圖中,你應該可以看到,原來附加到RID上面的U鎖,由於PagLock的提升,現在要附加到Page上面了,這個就是所謂的資料頁鎖。
4.TABLOCKX,ROWLOCK
這兩個我就不細說了,TABLOCKX就是直接附加在table上的X鎖,你可以通過select看一下。
ROWLOCK的話,預設情況下就是ROWLOCK,比如預設的Update,你會發現RID上被附加的U鎖,這個就是行鎖。
5.UPDLOCK
這個鎖還是蠻有意思的,它就是update鎖,如果你select下,它會呈現update的鎖痙攣效果。
6.XLOCK
知道了UPDLOCK鎖,我想XLOCK你也應該明白了。。。它就是delete鎖,即排他鎖,我可以讓select帶上排他鎖。
7.HOLDLOCK
最後一個我也沒鬧明白,據說是讓語句在整個事務中持有鎖,然後我就用select和update除錯一下。
1 SELECT * FROM dbo.Person(HOLDLOCK) 2 UPDATE dbo.Person WITH(HOLDLOCK) SET NAME='bbbbb' WHERE ID=4
三 SQL Server 鎖機制 悲觀鎖 樂觀鎖 實測解析
在使用SQL時,大都會遇到這樣的問題,你Update一條記錄時,需要通過Select來檢索出其值或條件,然後在通過這個值來執行修改操作。
但當以上操作放到多執行緒中併發處理時會出現問題:某執行緒select了一條記錄但還沒來得及update時,另一個執行緒仍然可能會進來select到同一條記錄。
一般解決辦法就是使用鎖和事物的聯合機制:
1. 把select放在事務中, 否則select完成, 鎖就釋放了
2. 要阻止另一個select , 則要手工加鎖, select 預設是共享鎖, select之間的共享鎖是不衝突的, 所以, 如果只是共享鎖, 即使鎖沒有釋放, 另一個select一樣可以下共享鎖, 從而select出資料
- BEGINTRAN
- SELECT*FROMTableWITH(UPDLOCK)
- --或者SELECT*FROMTableWITH(TABLOCKX,READPAST)具體情況而定。
- UPDATE....
- COMMITTRAN
所有Select加 With (NoLock)解決阻塞死鎖,在查詢語句中使用 NOLOCK 和 READPAST
處理一個數據庫死鎖的異常時候,其中一個建議就是使用 NOLOCK 或者 READPAST 。有關 NOLOCK 和 READPAST的一些技術知識點:
對於非銀行等嚴格要求事務的行業,搜尋記錄中出現或者不出現某條記錄,都是在可容忍範圍內,所以碰到死鎖,應該首先考慮,我們業務邏輯是否能容忍出現或者不出現某些記錄,而不是尋求對雙方都加鎖條件下如何解鎖的問題。
NOLOCK 和 READPAST 都是處理查詢、插入、刪除等操作時候,如何應對鎖住的資料記錄。但是這時候一定要注意NOLOCK 和 READPAST的侷限性,確認你的業務邏輯可以容忍這些記錄的出現或者不出現:
簡單來說:
1.NOLOCK 可能把沒有提交事務的資料也顯示出來
2.READPAST 會把被鎖住的行不顯示出來
不使用 NOLOCK 和 READPAST ,在 Select 操作時候則有可能報錯誤:事務(程序 ID **)與另一個程序被死鎖在 鎖 資源上,並且已被選作死鎖犧牲品。
SELECT * FROM Table WITH(NOLOCK)
SELECT * FROM Table WITH(READPAST)
實際開始動手用程式碼說話吧!
SQLServer2012在查詢分析器裡面開兩個連線
插入鎖:
結論:“表鎖”鎖定對該表的Select、Update、Delete操作,但不影響對該表的Insert操作也不影響以主鍵Id為條件的Select,所以Select如果不想等待就要在Select後加With(Nolock),但這樣會產生髒資料就是其他事務已更新但並沒有提交的資料,如果該事務進行了RollBack則取出的資料就是錯誤的,所以好自己權衡利弊,一般情況下90%以上的Select都允許髒讀,只有賬戶金額相關的不允許。
- ------------------A連線InsertLock-------------------
- BEGINTRAN
- INSERTINTOdbo.UserInfo
- (Name,Age,Mobile,AddTime,Type)
- VALUES('eee',--Name-varchar(50)
- 2,--Age-int
- '555',--Mobile-char(11)
- GETDATE(),--AddTime-datetime
- 0--Type-int
- )
- SELECTresource_type,request_mode,COUNT(*)FROMsys.dm_tran_locks
- WHERErequest_session_id=@@SPID
- GROUPBYresource_type,request_mode
- --ROLLBACKTRAN
- ------------------------B連線InsertLock------------------------
- INSERTINTOdbo.UserInfo
- (Name,Age,Mobile,AddTime,Type)
- VALUES('fff',--Name-varchar(50)
- 2,--Age-int
- '123',--Mobile-char(11)
- GETDATE(),--AddTime-datetime
- 1--Type-int
- )--可以執行插入
- SELECT*FROMdbo.UserInfo--需要等待解鎖
- SELECT*FROMdbo.UserInfoWHEREAge=1--需要等待解鎖
- SELECT*FROMdbo.UserInfoWHEREId=3--可以執行查詢(根據主鍵可以)
- SELECT*FROMdbo.UserInfoWITH(NOLOCK)--可以執行查詢(在一個事務中,有更新欄位但還沒有提交,此時就會查處髒資料)
- SELECT*FROMdbo.UserInfoWITH(NOLOCK)WHEREAge=1--可以執行查詢
- UPDATEdbo.UserInfoSETType=5WHEREName='fff'--需要等待解鎖
- DELETEFROMdbo.UserInfoWHEREName='fff'--需要等待解鎖
更新鎖:
結論:“表鎖”鎖定對該表的Select、Update、Delete操作,但不影響對該表的Insert操作也不影響以主鍵Id為條件的Select
- -----------------------A連線UpdateLock-----------------------
- BEGINTRAN
- UPDATEdbo.UserInfoSETName='eee'WHEREAge=2
- SELECTresource_type,request_mode,COUNT(*)FROMsys.dm_tran_locks
- WHERErequest_session_id=@@SPID
- GROUPBYresource_type,request_mode
- --ROLLBACKTRAN
- ------------------------B連線UpdateLock------------------------
- INSERTINTOdbo.UserInfo
- (Name,Age,Mobile,AddTime,Type)
- VALUES('ppp',--Name-varchar(50)
- 15,--Age-int
- '666',--Mobile-char(11)
- GETDATE(),--AddTime-datetime
- 9--Type-int
- )--可以執行插入
- SELECT*FROMdbo.UserInfo--需要等待解鎖
- SELECT*FROMdbo.UserInfoWHEREName='ppp'--需要等待解鎖
- SELECT*FROMdbo.UserInfoWHEREId=3--可以執行查詢(根據主鍵可以)
- SELECT*FROMdbo.UserInfoWITH(NOLOCK)--可以執行查詢(在一個事務中,有更新欄位但還沒有提交,此時就會查處髒資料)
- SELECT*FROMdbo.UserInfoWITH(NOLOCK)WHEREName='ppp'--可以執行查詢
- UPDATEdbo.UserInfoSETAge=8WHEREName='ccc'--需要等待解鎖
- DELETEdbo.UserInfoWHEREAge=5--需要等待解鎖
主鍵鎖:
結論:“行鎖+表鎖” 鎖定對該表的Select、Update、Delete操作,但不影響對該表的Insert操作也不影響以主鍵Id為條件的Select、Update、Delete
- ------------------------A連線KeyLock--------------------
- BEGINTRAN
- UPDATEdbo.UserInfoSETName='hhh'WHEREId=3--以主鍵為條件
- SELECTresource_type,request_mode,COUNT(*)FROMsys.dm_tran_locks
- WHERErequest_session_id=@@SPID
- GROUPBYresource_type,request_mode
- --ROLLBACKTRAN
- ------------------------B連線KeyLock----------------------
- INSERTINTOdbo.UserInfo
- (Name,Age,Mobile,AddTime,Type)
- VALUES('kkk',--Name-varchar(50)
- 18,--Age-int
- '234',--Mobile-char(11)
- GETDATE(),--AddTime-datetime
- 7--Type-int
- )--可以執行插入
- SELECT*FROMdbo.UserInfoWITH(NOLOCK)--可以執行查詢(在一個事務中,有更新欄位但還沒有提交,此時就會查處髒資料)
- SELECT*FROMdbo.UserInfoWITH(NOLOCK)WHEREName='kkk'--可以執行查詢
- -----//全表查詢及操作正在處理的行
- SELECT*FROMdbo.UserInfo--需要等待解鎖
- SELECT*FROMdbo.UserInfoWHEREId=3--需要等待解鎖(根據主鍵,但與A連線操作相同行不可)
- UPDATEdbo.UserInfoSETName='mmm'WHEREId=3--需要等待解鎖(根據主鍵,但與A連線操作相同行不可)
- DELETEdbo.UserInfoWHEREId=3--需要等待解鎖(根據主鍵,但與A連線操作相同行不可)
- -----//使用非主鍵為條件的操作
- SELECT*FROMdbo.UserInfoWHEREName='aaa'--需要等待解鎖(非主鍵不可)
- UPDATEdbo.UserInfoSETName='ooo'WHEREName='aaa'--需要等待解鎖(非主鍵不可)
- DELETEdbo.UserInfoWHEREName='aaa'--需要等待解鎖(非主鍵不可)
- -----//使用主鍵為條件的操作
- SELECT*FROMdbo.UserInfoWHEREid=1--可以執行查詢(根據主鍵可以)
- UPDATEdbo.UserInfoSETName='yyy'WHEREId=1--可以執行更新(根據主鍵可以)
- DELETEdbo.UserInfoWHEREId=1--可以執行刪除(根據主鍵可以)
索引鎖:
結論:“行鎖+表鎖” 鎖定對該表的Select、Update、Delete操作,但不影響對該表的Insert操作也不影響以主鍵Id為條件的Select、Update、Delete,也不影響以索引列Name為條件的Update、Delete但不可以Select
- ------------------------A連線IndexLock--------------------
- DROPINDEXdbo.UserInfo.Index_UserInfo_Name
- CREATEINDEXIndex_UserInfo_NameONdbo.UserInfo(Name)
- BEGINTRAN
- UPDATEdbo.UserInfoSETage=66WHEREName='ddd'--使用name索引列為條件
- SELECTresource_type,request_mode,COUNT(*)FROMsys.dm_tran_locks
- WHERErequest_session_id=@@SPID
- GROUPBYresource_type,request_mode
- --ROLLBACKTRAN
- ----------------------B連線IndexLock-------------------
- INSERTINTOdbo.UserInfo
- (Name,Age,Mobile,AddTime,Type)
- VALUES('iii',--Name-varchar(50)
- 20,--Age-int
- '235235235',--Mobile-char(11)
- GETDATE(),--AddTime-datetime
- 12--Type-int
- )--可以執行插入
- SELECT*FROMdbo.UserInfoWITH(NOLOCK)--可以執行查詢(在一個事物中,有更新欄位但還沒有提交,此時就會查處髒資料)
- SELECT*FROMdbo.UserInfoWITH(NOLOCK)WHEREName='kkk'--可以執行查詢
- -----//全表查詢及操作正在處理的行
- SELECT*FROMdbo.UserInfo--需要等待解鎖
- SELECT*FROMdbo.UserInfoWHEREId=4--需要等待解鎖(根據主鍵,但與A連線操作相同行不可)
- UPDATEdbo.UserInfoSETName='mmm'WHEREId=4--需要等待解鎖(根據主鍵,但與A連線操作相同行不可)
- DELETEdbo.UserInfoWHEREId=4--需要等待解鎖(根據主鍵,但與A連線操作相同行不可)
- -----//使用非主鍵非索引為條件的操作
- SELECT*FROMdbo.UserInfoWHEREAge=5--需要等待解鎖(非主鍵不可)
- UPDATEdbo.UserInfoSETName='ooo'WHEREAge=5--需要等待解鎖(非主鍵不可)
- DELETEdbo.UserInfoWHEREAge=5--需要等待解鎖(非主鍵不可)
- -----//使用主鍵為條件的操作
- SELECT*FROMdbo.UserInfoWHEREId=1--可以執行更新(根據主鍵可以)
- UPDATEdbo.UserInfoSETName='yyy'WHEREId=1--可以執行更新(根據主鍵可以)
- DELETEdbo.UserInfoWHEREId=1--可以執行刪除(根據主鍵可以)
- -----//使用索引為條件的操作
- SELECT*FROMdbo.UserInfoWHEREName='aaa'--需要等待解鎖(非主鍵不可)
- UPDATEdbo.UserInfoSETName='ooo'WHEREName='aaa'--可以執行更新(根據索引可以)
- DELETEdbo.UserInfoWHEREName='aaa'--可以執行刪除(根據索引可以)
悲觀鎖(更新鎖-人工手動設定上鎖):
結論:可以理解為在使用版本控制軟體的時候A遷出了一個檔案,並且8i將這個87檔案鎖定,B就無法再遷出該檔案了,直到A遷入解鎖後才能被其他人遷出。
- ------------------------A連線UpdateLock(悲觀鎖)---------------------
- BEGINTRAN
- SELECT*FROMdbo.UserInfoWITH(UPDLOCK)WHEREId=2
- SELECTresource_type,request_mode,COUNT(*)FROMsys.dm_tran_locks
- WHERErequest_session_id=@@SPID
- GROUPBYresource_type,request_mode
- --COMMITTRAN
- --ROLLBACKTRAN
- ---------------------------B連線UpdateLock(悲觀鎖)-------------------------
- SELECT*FROMdbo.UserInfo--可以執行查詢
- SELECT*FROMdbo.UserInfoWHEREid=2--可以執行查詢
- SELECT*FROMdbo.UserInfoWHEREName='ooo'--可以執行查詢
- UPDATEdbo.UserInfoSETAge=3WHEREid=1--可以執行更新(根據主鍵可以)
- UPDATEdbo.UserInfoSETAge=3WHEREName='ccc'--需要等待解鎖(非主鍵不可)
- DELETEdbo.UserInfoWHEREid=1--可以執行更新(根據主鍵可以)
- DELETEdbo.UserInfoWHEREname='ccc'--需要等待解鎖(非主鍵不可)
樂觀鎖(人工通過邏輯在資料庫中模擬鎖)
結論:可以理解為同樣在使用版本控制軟體的時候A遷出了一個檔案,B也可以遷出該檔案,兩個人都可以對此檔案進行修改,其中一個人先進行提交的時候,版本並沒有變化所以可以正常提交,另一個後提交的時候,發現版本增加不對稱了,就提示衝突由使用者來選擇如何進行合併再重新進行提交。
- --------------------------A客戶端連線Lock(樂觀鎖)------------------------
- --DROPTABLECoupon
- -----------------建立優惠券表-----------------
- CREATETABLECoupon
- (
- IdINTPRIMARYKEYIDENTITY(1,1),
- NumberVARCHAR(50)NOTNULL,
- [User]VARCHAR(50),
- UseTimeDATETIME,
- IsFlagBITDEFAULT(0)NOTNULL,
- CreateTimeDATETIMEDEFAULT(GETDATE())NOTNULL
- )
- INSERTINTOdbo.Coupon(Number)VALUES('10000001')
- INSERTINTOdbo.Coupon(Number)VALUES('10000002')
- INSERTINTOdbo.Coupon(Number)VALUES('10000003')
- INSERTINTOdbo.Coupon(Number)VALUES('10000004')
- INSERTINTOdbo.Coupon(Number)VALUES('10000005')
- INSERTINTOdbo.Coupon(Number)VALUES('10000006')
- --SELECT*FROMdbo.CouponWITH(NOLOCK)--查詢資料
- --UPDATECouponSET[User]=NULL,UseTime=NULL,IsFlag=0--還原資料
- -----------------1、模擬高併發普通更新-----------------
- DECLARE@UserVARCHAR(50)--模擬要使用優惠券的使用者
- DECLARE@TempIdINT--模擬抽選出來的要使用的優惠券
- SET@User='a'
- BEGINTRAN
- SELECT@TempId=IdFROMdbo.CouponWHEREIsFlag=0--高併發時此語句有可能另外一個該事務已取出的Id
- --WAITFORDELAY'00:00:05'--改用此方式要開兩個SQLManagement客戶端
- UPDATEdbo.CouponSETIsFlag=1,[User]=@User,UseTime=GETDATE()WHEREId=@TempId
- COMMITTRAN
- --ROLLBACKTRAN
- -----------------2、悲觀鎖解決方案-----------------
- DECLARE@UserVARCHAR(50)--模擬要使用優惠券的使用者
- DECLARE@TempIdINT--模擬抽選出來的要使用的優惠券
- SET@User='a'
- BEGINTRAN
- SELECT@TempId=IdFROMdbo.CouponWITH(UPDLOCK)WHEREIsFlag=0--高併發時此語句會鎖定取出的Id資料行
- --WAITFORDELAY'00:00:05'--改用此方式要開兩個SQLManagement客戶端
- UPDATEdbo.CouponSETIsFlag=1,[User]=@User,UseTime=GETDATE()WHEREId=@TempId
- COMMITTRAN
- --ROLLBACKTRAN
- -----------------3、樂觀鎖解決方案-----------------
- ALTERTABLEdbo.CouponADDRowVerROWVERSIONNOTNULL--增加資料行版本戳型別欄位(微軟新推薦資料欄位,該欄位每張表只能有一個,會在建立行或更新行時自動進行修改無需人為干涉,該欄位不能建立索引及主鍵因為會頻繁修改)
- DECLARE@UserVARCHAR(50)--模擬要使用優惠券的使用者
- DECLARE@TempIdINT--模擬抽選出來的要使用的優惠券
- DECLARE@RowVerBINARY(8)--抽選出來的優惠券的版本(ROWVERSION資料型別儲存大小為8位元組)
- SET@User='a'
- BEGINTRY
- BEGINTRAN
- SELECT@TempId=Id,@RowVer=RowVerFROMdbo.CouponWHEREIsFlag=0--取出可用的Id及對應的版本戳
- --WAITFORDELAY'00:00:05'--改用此方式要開兩個SQLManagement客戶端
- UPDATEdbo.CouponSETIsFlag=1,[User]=@User,UseTime=GETDATE()WHEREId=@TempIdANDRowVer=@RowVer
- IF(@@ROWCOUNT>0)
- BEGIN
- PRINT('修改成功')
- COMMITTRAN
- END
- ELSE
- BEGIN
- PRINT('該資料已被其他使用者修改')
- ROLLBACKTRAN
- END
- ENDTRY
- BEGINCATCH
- ROLLBACKTRAN
- ENDCATCH
- --------------------------B客戶端連線Lock(樂觀鎖)------------------------
- --此測試需要開兩個SQLManagementStudio客戶端,在A客戶端使用WAITFORDELAY來模擬併發佔用,在B客戶端執行與A客戶端相同的SQL指令碼即可(註釋掉WAITFOR),所以在此不放相同程式碼了。
在樂觀鎖和悲觀鎖之間進行選擇的標準是:衝突的頻率與嚴重性。如果衝突很少,或者衝突的後果不會很嚴重,那麼通常情況下應該選擇樂觀鎖,因為它能得到更好的併發性,而且更容易實現。但是,如果衝突的結果對於使用者來說痛苦的,那麼就需要使用悲觀策略。
我認為如果同一張表的併發很高,但併發處理同一條資料的衝突機率很低,那就應該使用樂觀鎖,反之,如果同一張表的併發不高,但同時處理同一條資料的機率很高,就應該使用悲觀鎖。
四 SQL Server 中WITH (NOLOCK)淺析
概念介紹
開發人員喜歡在SQL指令碼中使用WITH(NOLOCK), WITH(NOLOCK)其實是表提示(table_hint)中的一種。它等同於 READUNCOMMITTED 。 具體的功能作用如下所示(摘自MSDN):
1: 指定允許髒讀。不釋出共享鎖來阻止其他事務修改當前事務讀取的資料,其他事務設定的排他鎖不會阻礙當前事務讀取鎖定資料。允許髒讀可能產生較多的併發操作,但其代價是讀取以後會被其他事務回滾的資料修改。這可能會使您的事務出錯,向用戶顯示從未提交過的資料,或者導致使用者兩次看到記錄(或根本看不到記錄)。有關髒讀、不可重複讀和幻讀的詳細資訊,請參閱併發影響。
2: READUNCOMMITTED 和 NOLOCK 提示僅適用於資料鎖。所有查詢(包括那些帶有 READUNCOMMITTED 和 NOLOCK 提示的查詢)都會在編譯和執行過程中獲取 Sch-S(架構穩定性)鎖。因此,當併發事務持有表的 Sch-M(架構修改)鎖時,將阻塞查詢。例如,資料定義語言 (DDL) 操作在修改表的架構資訊之前獲取 Sch-M 鎖。所有併發查詢(包括那些使用 READUNCOMMITTED 或 NOLOCK 提示執行的查詢)都會在嘗試獲取 Sch-S 鎖時被阻塞。相反,持有 Sch-S 鎖的查詢將阻塞嘗試獲取 Sch-M 鎖的併發事務。有關鎖行為的詳細資訊,請參閱鎖相容性(資料庫引擎)。
3: 不能為通過插入、更新或刪除操作修改過的表指定 READUNCOMMITTED 和 NOLOCK。SQL Server 查詢優化器忽略 FROM 子句中應用於 UPDATE 或 DELETE 語句的目標表的 READUNCOMMITTED 和 NOLOCK 提示。
功能與缺陷
使用WIHT(NOLOCK)有利也有弊,所以在決定使用之前,你一定需要了解清楚WITH(NOLOCK)的功能和缺陷,看其是否適合你的業務需求,不要覺得它能提升效能,稀裡糊塗的就使用它。
1:使用WITH(NOLOCK)時查詢不受其它排他鎖阻塞
開啟會話視窗1,執行下面指令碼,不提交也不回滾事務,模擬事務真在執行過程當中
BEGIN TRAN
UPDATE TEST SET NAME='Timmy' WHERE OBJECT_ID =1;
--ROLLBACK
開啟會話視窗2,執行下面指令碼,你會發現執行結果一直查詢不出來(其實才兩條記錄)。當前會話被阻塞了
SELECT * FROM TEST;
開啟會話視窗3,執行下面指令碼,檢視阻塞情況,你會發現在會話2被會話1給阻塞了,會話2的等待型別為LCK_M_S:“當某任務正在等待獲取共享鎖時出現”
SELECT wt.blocking_session_id AS BlockingSessesionId
,sp.program_name AS ProgramName
,COALESCE(sp.LOGINAME, sp.nt_username) AS HostName
,ec1.client_net_address AS ClientIpAddress
,db.name AS DatabaseName
,wt.wait_type AS WaitType
,ec1.connect_time AS BlockingStartTime
,wt.WAIT_DURATION_MS/1000 AS WaitDuration
,ec1.session_id AS BlockedSessionId
,h1.TEXT AS BlockedSQLText
,h2.TEXT AS BlockingSQLText
FROM sys.dm_tran_locks AS tl
INNER JOIN sys.databases db
ON db.database_id = tl.resource_database_id
INNER JOIN sys.dm_os_waiting_tasks AS wt
ON tl.lock_owner_address = wt.resource_address
INNER JOIN sys.dm_exec_connections ec1
ON ec1.session_id = tl.request_session_id
INNER JOIN sys.dm_exec_connections ec2
ON ec2.session_id = wt.blocking_session_id
LEFT OUTER JOIN master.dbo.sysprocesses sp
ON SP.spid = wt.blocking_session_id
CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) AS h1
CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) AS h2
此時檢視會話1(會話1的會話ID為53,執行指令碼1前,可以用SELECT @@spid檢視會話ID)的鎖資訊情況,你會發現表TEST(ObjId=1893581784)持有的鎖資訊如下所示
開啟會話視窗4,執行下面指令碼.你會發現查詢結果很快就出來,會話4並不會被會話1阻塞。
SELECT * FROM TEST WITH(NOLOCK)
從上面模擬的這個小例子可以看出,正是由於加上WITH(NOLOCK)提示後,會話1中事務設定的排他鎖不會阻礙當前事務讀取鎖定資料,所以會話4不會被阻塞,從而提升併發時查詢效能。
2:WITH(NOLOCK) 不釋出共享鎖來阻止其他事務修改當前事務讀取的資料,這個就不舉例子了。
本質上WITH(NOLOCK)是通過減少鎖和不受排它鎖影響來減少阻塞,從而提高併發時的效能。所謂凡事有利也有弊,WITH(NOLOCK)在提升效能的同時,也會產生髒讀現象。
如下所示,表TEST有兩條記錄,我準備更新OBJECT_ID=1的記錄,此時事務既沒有提交也沒有回滾
BEGIN TRAN
UPDATE TEST SET NAME='Timmy' WHERE OBJECT_ID =1;
--ROLLBACK
此時另外一個會話使用WITH(NOLOCK)查到的記錄為未提交的記錄值
假如由於某種原因,該事務回滾了,那麼我們讀取到的OBJECT_ID=1的記錄就是一條髒資料。
髒讀又稱無效資料的讀出,是指在資料庫訪問中,事務T1將某一值修改,然後事務T2讀取該值,此後T1因為某種原因撤銷對該值的修改,這就導致了T2所讀取到的資料是無效的。
WITH(NOLOCK)使用場景
什麼時候可以使用WITH(NOLOCK)? 什麼時候不能使用WITH(NOLOCK),這個要視你係統業務情況,綜合考慮效能情況與業務要求來決定是否使用WITH(NOLOCK), 例如涉及到金融或會計成本之類的系統,出現髒讀那是要產生嚴重問題的。關鍵業務系統也要慎重考慮。大體來說一般有下面一些場景可以使用WITH(NOLOCK)
1: 基礎資料表,這些表的資料很少變更。
2:歷史資料表,這些表的資料很少變更。
3:業務允許髒讀情況出現涉及的表。
4:資料量超大的表,出於效能考慮,而允許髒讀。
另外一點就是不要濫用WITH(NOLOCK),我發現有個奇怪現象,很多開發知道WITH(NOLOCK),但是有不瞭解髒讀,習慣性的使用WITH(NOLOCK)。
WITH(NOLOCK)與 NOLOCK區別
為了搞清楚WITH(NOLOCK)與NOLOCK的區別,我查了大量的資料,我們先看看下面三個SQL語句有啥區別
SELECT * FROM TEST NOLOCK
SELECT * FROM TEST (NOLOCK);
SELECT * FROM TEST WITH(NOLOCK);
上面的問題概括起來也就是說NOLOCK、(NOLOCK)、 WITH(NOLOCK)的區別:
1: NOLOCK這樣的寫法,其實NOLOCK其實只是別名的作用,而沒有任何實質作用。所以不要粗心將(NOLOCK)寫成NOLOCK
2:(NOLOCK)與WITH(NOLOCK)其實功能上是一樣的。(NOLOCK)只是WITH(NOLOCK)的別名,但是在SQL Server 2008及以後版本中,(NOLOCK)不推薦使用了,"不借助 WITH 關鍵字指定表提示”的寫法已經過時了。具體參見MSDNhttp://msdn.microsoft.com/zh-cn/library/ms143729%28SQL.100%29.aspx
2.1 至於網上說WITH(NOLOCK)在SQL SERVER 2000不生效,我驗證後發現完全是個謬論。
2.2 在使用連結伺服器的SQL當中,(NOLOCK)不會生效,WITH(NOLOCK)才會生效。如下所示
訊息 4122,級別 16,狀態 1,第 1 行
Remote table-valued function calls are not allowed.
3.語法上有些許出入,如下所示
這種語法會報錯
SELECT * FROM sys.indexes WITH(NOLOCK) AS i
-Msg 156, Level 15, State 1, Line 1
-Incorrect syntax near the keyword 'AS'.
這種語法正常
SELECT * FROM sys.indexes (NOLOCK) AS i
可以全部改寫為下面語法
SELECT * FROM sys.indexes i WITH(NOLOCK)
SELECT * FROM sys.indexes i (NOLOCK)
WITH(NOLOCK)會不會產生鎖
很多人誤以為使用了WITH(NOLOCK)後,資料庫庫不會產生任何鎖。實質上,使用了WITH(NOLOCK)後,資料庫依然對該表物件生成Sch-S(架構穩定性)鎖以及DB型別的共享鎖, 如下所示,可以在一個會話中查詢一個大表,然後在另外一個會話中檢視鎖資訊(也可以使用SQL Profile檢視會話鎖資訊)
不使用WTIH(NOLOCK)
使用WITH(NOLOCK)
從上可以看出使用WITH(NOLOCK)後,資料庫並不是不生成相關鎖。 對比可以發現使用WITH(NOLOCK)後,資料庫只會生成DB型別的共享鎖、以及TAB型別的架構穩定性鎖.
另外,使用WITH(NOLOCK)並不是說就不會被其它會話阻塞,依然可能會產生Schema Change Blocking
會話1:執行下面SQL語句,暫時不提交,模擬事務正在執行
BEGIN TRAN
ALTER TABLE TEST ADD Grade VARCHAR(10) ;
會話2:執行下面語句,你會發現會話被阻塞,截圖如下所示。
SELECT * FROM TEST WITH(NOLOCK)
-----
本文引用了以下網址內容:
http://www.cnblogs.com/huangxincheng/p/4292320.html
http://blog.itpub.NET/13651903/viewspace-1091664/
http://www.cnblogs.com/kerrycode/p/3946268.html
http://www.cnblogs.com/taiyonghai/p/5674462.html
--- end ---