sql server鎖(lock)知識及鎖應用http://blog.csdn.net/huwei2003/article/details/4047191
一 關於鎖的基礎知識
(一). 為什么要引入鎖
當多個用戶同時對數據庫的並發操作時會帶來以下數據不一致的問題:
◆丟失更新
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解鎖
◆排它鎖
新建兩個連接,在第一個連接中執行以下語句
- begin tran
- update table1 set A='aa' where B='b2'
- waitfor delay '00:00:30' --等待30秒
- commit tran
- --在第二個連接中執行以下語句
- begin tran
- select * from table1 where B='b2'
- commit tran
若同時執行上述兩個語句,則select查詢必須等待update執行完畢才能執行即要等待30秒
◆共享鎖
在第一個連接中執行以下語句
- begin tran
- select * from table1 holdlock --holdlock人為加鎖
- where B='b2'
- waitfor delay '00:00:30' --等待30秒
- commit tran
◆共享鎖
在第一個連接中執行以下語句
- begin tran
- select * from table1 holdlock --holdlock人為加鎖
- where B='b2'
- waitfor delay '00:00:30' --等待30秒
- commit tran
在第二個連接中執行以下語句
- begin tran
- select A,C from table1 where B='b2'
- update table1 set A='aa' where B='b2'
- commit tran
若同時執行上述兩個語句,則第二個連接中的select查詢可以執行
而update必須等待第一個事務釋放共享鎖轉為排它鎖后才能執行 即要等待30秒
◆死鎖
- --在第一個連接中執行以下語句
- begin tran
- update table1 set A='aa' where B='b2'
- waitfor delay '00:00:30'
- update table2 set D='d5' where E='e1'
- commit tran
- --在第二個連接中執行以下語句
- begin tran
- update table2 set D='d5' where E='e1'
- waitfor delay '00:00:10'
- update table1 set A='aa' where B='b2'
- commit tran
同時執行,系統會檢測出死鎖,並中止進程
十 應用程序鎖:
應用程序鎖就是客戶端代碼生成的鎖,而不是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出數據
- BEGIN TRAN
- SELECT * FROM Table WITH(UPDLOCK)
- --或者 SELECT * FROM Table WITH(TABLOCKX, READPAST) 具體情況而定。
- UPDATE ....
- COMMIT TRAN
所有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連接 Insert Lock-------------------
- BEGIN TRAN
- INSERT INTO dbo.UserInfo
- ( Name, Age, Mobile, AddTime, Type )
- VALUES ( 'eee', -- Name - varchar(50)
- 2, -- Age - int
- '555', -- Mobile - char(11)
- GETDATE(), -- AddTime - datetime
- 0 -- Type - int
- )
- SELECT resource_type, request_mode,COUNT(*) FROM sys.dm_tran_locks
- WHERE request_session_id=@@SPID
- GROUP BY resource_type,request_mode
- --ROLLBACK TRAN
- ------------------------B連接 Insert Lock------------------------
- INSERT INTO dbo.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 * FROM dbo.UserInfo --需要等待解鎖
- SELECT * FROM dbo.UserInfo WHERE Age=1 --需要等待解鎖
- SELECT * FROM dbo.UserInfo WHERE Id=3 --可以執行查詢(根據主鍵可以)
- SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以執行查詢(在一個事務中,有更新字段但還沒有提交,此時就會查處臟數據)
- SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Age=1 --可以執行查詢
- UPDATE dbo.UserInfo SET Type=5 WHERE Name='fff' --需要等待解鎖
- DELETE FROM dbo.UserInfo WHERE Name='fff' --需要等待解鎖
更新鎖:
結論:“表鎖”鎖定對該表的Select、Update、Delete操作,但不影響對該表的Insert操作也不影響以主鍵Id為條件的Select
- -----------------------A連接 Update Lock-----------------------
- BEGIN TRAN
- UPDATE dbo.UserInfo SET Name = 'eee' WHERE Age = 2
- SELECT resource_type, request_mode,COUNT(*) FROM sys.dm_tran_locks
- WHERE request_session_id=@@SPID
- GROUP BY resource_type,request_mode
- --ROLLBACK TRAN
- ------------------------B連接 Update Lock------------------------
- INSERT INTO dbo.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 * FROM dbo.UserInfo --需要等待解鎖
- SELECT * FROM dbo.UserInfo WHERE Name='ppp' --需要等待解鎖
- SELECT * FROM dbo.UserInfo WHERE Id=3 --可以執行查詢(根據主鍵可以)
- SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以執行查詢(在一個事務中,有更新字段但還沒有提交,此時就會查處臟數據)
- SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Name = 'ppp' --可以執行查詢
- UPDATE dbo.UserInfo SET Age=8 WHERE Name='ccc' --需要等待解鎖
- DELETE dbo.UserInfo WHERE Age = 5 --需要等待解鎖
主鍵鎖:
結論:“行鎖+表鎖” 鎖定對該表的Select、Update、Delete操作,但不影響對該表的Insert操作也不影響以主鍵Id為條件的Select、Update、Delete
- ------------------------A連接 Key Lock--------------------
- BEGIN TRAN
- UPDATE dbo.UserInfo SET Name='hhh' WHERE Id=3 --以主鍵為條件
- SELECT resource_type, request_mode,COUNT(*) FROM sys.dm_tran_locks
- WHERE request_session_id=@@SPID
- GROUP BY resource_type,request_mode
- --ROLLBACK TRAN
- ------------------------B連接 Key Lock----------------------
- INSERT INTO dbo.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 * FROM dbo.UserInfo WITH(NOLOCK) --可以執行查詢(在一個事務中,有更新字段但還沒有提交,此時就會查處臟數據)
- SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Name = 'kkk' --可以執行查詢
- -----//全表查詢及操作正在處理的行
- SELECT * FROM dbo.UserInfo --需要等待解鎖
- SELECT * FROM dbo.UserInfo WHERE Id=3 --需要等待解鎖(根據主鍵,但與A連接操作相同行不可)
- UPDATE dbo.UserInfo SET Name='mmm' WHERE Id=3 --需要等待解鎖(根據主鍵,但與A連接操作相同行不可)
- DELETE dbo.UserInfo WHERE Id=3 --需要等待解鎖(根據主鍵,但與A連接操作相同行不可)
- -----//使用非主鍵為條件的操作
- SELECT * FROM dbo.UserInfo WHERE Name='aaa' --需要等待解鎖(非主鍵不可)
- UPDATE dbo.UserInfo SET Name='ooo' WHERE Name='aaa' --需要等待解鎖(非主鍵不可)
- DELETE dbo.UserInfo WHERE Name='aaa' --需要等待解鎖(非主鍵不可)
- -----//使用主鍵為條件的操作
- SELECT * FROM dbo.UserInfo WHERE id=1 --可以執行查詢(根據主鍵可以)
- UPDATE dbo.UserInfo SET Name='yyy' WHERE Id=1 --可以執行更新(根據主鍵可以)
- DELETE dbo.UserInfo WHERE Id=1 --可以執行刪除(根據主鍵可以)
索引鎖:
結論:“行鎖+表鎖” 鎖定對該表的Select、Update、Delete操作,但不影響對該表的Insert操作也不影響以主鍵Id為條件的Select、Update、Delete,也不影響以索引列Name為條件的Update、Delete但不可以Select
- ------------------------A連接 Index Lock--------------------
- DROP INDEX dbo.UserInfo.Index_UserInfo_Name
- CREATE INDEX Index_UserInfo_Name ON dbo.UserInfo(Name)
- BEGIN TRAN
- UPDATE dbo.UserInfo SET age=66 WHERE Name='ddd' --使用name索引列為條件
- SELECT resource_type, request_mode,COUNT(*) FROM sys.dm_tran_locks
- WHERE request_session_id=@@SPID
- GROUP BY resource_type,request_mode
- --ROLLBACK TRAN
- ----------------------B連接 Index Lock-------------------
- INSERT INTO dbo.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 * FROM dbo.UserInfo WITH(NOLOCK) --可以執行查詢(在一個事物中,有更新字段但還沒有提交,此時就會查處臟數據)
- SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Name = 'kkk' --可以執行查詢
- -----//全表查詢及操作正在處理的行
- SELECT * FROM dbo.UserInfo --需要等待解鎖
- SELECT * FROM dbo.UserInfo WHERE Id=4 --需要等待解鎖(根據主鍵,但與A連接操作相同行不可)
- UPDATE dbo.UserInfo SET Name='mmm' WHERE Id=4 --需要等待解鎖(根據主鍵,但與A連接操作相同行不可)
- DELETE dbo.UserInfo WHERE Id=4 --需要等待解鎖(根據主鍵,但與A連接操作相同行不可)
- -----//使用非主鍵非索引為條件的操作
- SELECT * FROM dbo.UserInfo WHERE Age=5 --需要等待解鎖(非主鍵不可)
- UPDATE dbo.UserInfo SET Name='ooo' WHERE Age=5 --需要等待解鎖(非主鍵不可)
- DELETE dbo.UserInfo WHERE Age=5 --需要等待解鎖(非主鍵不可)
- -----//使用主鍵為條件的操作
- SELECT * FROM dbo.UserInfo WHERE Id=1 --可以執行更新(根據主鍵可以)
- UPDATE dbo.UserInfo SET Name='yyy' WHERE Id=1 --可以執行更新(根據主鍵可以)
- DELETE dbo.UserInfo WHERE Id=1 --可以執行刪除(根據主鍵可以)
- -----//使用索引為條件的操作
- SELECT * FROM dbo.UserInfo WHERE Name='aaa' --需要等待解鎖(非主鍵不可)
- UPDATE dbo.UserInfo SET Name='ooo' WHERE Name='aaa' --可以執行更新(根據索引可以)
- DELETE dbo.UserInfo WHERE Name='aaa' --可以執行刪除(根據索引可以)
悲觀鎖(更新鎖-人工手動設置上鎖):
結論:可以理解為在使用版本控制軟件的時候A遷出了一個文件,並且8i將這個87文件鎖定,B就無法再遷出該文件了,直到A遷入解鎖后才能被其他人遷出。
- ------------------------A連接 Update Lock(悲觀鎖)---------------------
- BEGIN TRAN
- SELECT * FROM dbo.UserInfo WITH(UPDLOCK) WHERE Id=2
- SELECT resource_type, request_mode,COUNT(*) FROM sys.dm_tran_locks
- WHERE request_session_id=@@SPID
- GROUP BY resource_type,request_mode
- --COMMIT TRAN
- --ROLLBACK TRAN
- ---------------------------B連接 Update Lock(悲觀鎖)-------------------------
- SELECT * FROM dbo.UserInfo --可以執行查詢
- SELECT * FROM dbo.UserInfo WHERE id=2 --可以執行查詢
- SELECT * FROM dbo.UserInfo WHERE Name='ooo' --可以執行查詢
- UPDATE dbo.UserInfo SET Age=3 WHERE id=1 --可以執行更新(根據主鍵可以)
- UPDATE dbo.UserInfo SET Age=3 WHERE Name='ccc' --需要等待解鎖(非主鍵不可)
- DELETE dbo.UserInfo WHERE id=1 --可以執行更新(根據主鍵可以)
- DELETE dbo.UserInfo WHERE name='ccc' --需要等待解鎖(非主鍵不可)
樂觀鎖(人工通過邏輯在數據庫中模擬鎖)
結論:可以理解為同樣在使用版本控制軟件的時候A遷出了一個文件,B也可以遷出該文件,兩個人都可以對此文件進行修改,其中一個人先進行提交的時候,版本並沒有變化所以可以正常提交,另一個后提交的時候,發現版本增加不對稱了,就提示沖突由用戶來選擇如何進行合並再重新進行提交。
- --------------------------A客戶端連接 Lock(樂觀鎖)------------------------
- --DROP TABLE Coupon
- -----------------創建優惠券表-----------------
- CREATE TABLE Coupon
- (
- Id INT PRIMARY KEY IDENTITY(1,1),
- Number VARCHAR(50) NOT NULL,
- [User] VARCHAR(50),
- UseTime DATETIME,
- IsFlag BIT DEFAULT(0) NOT NULL,
- CreateTime DATETIME DEFAULT(GETDATE()) NOT NULL
- )
- INSERT INTO dbo.Coupon(Number) VALUES ( '10000001')
- INSERT INTO dbo.Coupon(Number) VALUES ( '10000002')
- INSERT INTO dbo.Coupon(Number) VALUES ( '10000003')
- INSERT INTO dbo.Coupon(Number) VALUES ( '10000004')
- INSERT INTO dbo.Coupon(Number) VALUES ( '10000005')
- INSERT INTO dbo.Coupon(Number) VALUES ( '10000006')
- --SELECT * FROM dbo.Coupon WITH(NOLOCK) --查詢數據
- --UPDATE Coupon SET [User]=NULL, UseTime=NULL, IsFlag=0 --還原數據
- -----------------1、模擬高並發普通更新-----------------
- DECLARE @User VARCHAR(50) --模擬要使用優惠券的用戶
- DECLARE @TempId INT --模擬抽選出來的要使用的優惠券
- SET @User='a'
- BEGIN TRAN
- SELECT @TempId=Id FROM dbo.Coupon WHERE IsFlag=0 --高並發時此語句有可能另外一個該事務已取出的Id
- --WAITFOR DELAY '00:00:05' --改用此方式要開兩個SQL Management客戶端
- UPDATE dbo.Coupon SET IsFlag=1, [User]=@User, UseTime=GETDATE() WHERE Id=@TempId
- COMMIT TRAN
- --ROLLBACK TRAN
- -----------------2、悲觀鎖解決方案-----------------
- DECLARE @User VARCHAR(50) --模擬要使用優惠券的用戶
- DECLARE @TempId INT --模擬抽選出來的要使用的優惠券
- SET @User='a'
- BEGIN TRAN
- SELECT @TempId=Id FROM dbo.Coupon WITH(UPDLOCK) WHERE IsFlag=0 --高並發時此語句會鎖定取出的Id數據行
- --WAITFOR DELAY '00:00:05' --改用此方式要開兩個SQL Management客戶端
- UPDATE dbo.Coupon SET IsFlag=1, [User]=@User, UseTime=GETDATE() WHERE Id=@TempId
- COMMIT TRAN
- --ROLLBACK TRAN
- -----------------3、樂觀鎖解決方案-----------------
- ALTER TABLE dbo.Coupon ADD RowVer ROWVERSION NOT NULL --增加數據行版本戳類型字段(微軟新推薦數據字段,該字段每張表只能有一個,會在創建行或更新行時自動進行修改無需人為干涉,該字段不能建立索引及主鍵因為會頻繁修改)
- DECLARE @User VARCHAR(50) --模擬要使用優惠券的用戶
- DECLARE @TempId INT --模擬抽選出來的要使用的優惠券
- DECLARE @RowVer BINARY(8) --抽選出來的優惠券的版本(ROWVERSION數據類型存儲大小為8字節)
- SET @User='a'
- BEGIN TRY
- BEGIN TRAN
- SELECT @TempId=Id, @RowVer=RowVer FROM dbo.Coupon WHERE IsFlag=0 --取出可用的Id及對應的版本戳
- --WAITFOR DELAY '00:00:05' --改用此方式要開兩個SQL Management客戶端
- UPDATE dbo.Coupon SET IsFlag=1, [User]=@User, UseTime=GETDATE() WHERE Id=@TempId AND RowVer=@RowVer
- IF(@@ROWCOUNT > 0)
- BEGIN
- PRINT('修改成功')
- COMMIT TRAN
- END
- ELSE
- BEGIN
- PRINT('該數據已被其他用戶修改')
- ROLLBACK TRAN
- END
- END TRY
- BEGIN CATCH
- ROLLBACK TRAN
- END CATCH
- --------------------------B客戶端連接 Lock(樂觀鎖)------------------------
- --此測試需要開兩個SQL Management Studio客戶端,在A客戶端使用WAITFOR DELAY來模擬並發占用,在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 關鍵字指定表提示”的寫法已經過時了。 具體參見MSDN http://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 ---