提示:這里所摘抄的關(guān)于鎖的知識有的是不同sql server版本的,對應(yīng)于特定版本時會有問題。 一 關(guān)于鎖的基礎(chǔ)知識 (一). 為什么要引入鎖
當(dāng)多個用戶同時對數(shù)據(jù)庫的并發(fā)操作時會帶來以下數(shù)據(jù)不一致的問題: ◆丟失更新 A,B兩個用戶讀同一數(shù)據(jù)并進行修改,其中一個用戶的修改結(jié)果破壞了另一個修改的結(jié)果,比如訂票系統(tǒng) ◆臟讀 A用戶修改了數(shù)據(jù),隨后B用戶又讀出該數(shù)據(jù),但A用戶因為某些原因取消了對數(shù)據(jù)的修改,數(shù)據(jù)恢復(fù)原值,此時B得到的數(shù)據(jù)就與數(shù)據(jù)庫內(nèi)的數(shù)據(jù)產(chǎn)生了不一致 ◆不可重復(fù)讀 A用戶讀取數(shù)據(jù),隨后B用戶讀出該數(shù)據(jù)并修改,此時A用戶再讀取數(shù)據(jù)時發(fā)現(xiàn)前后兩次的值不一致 并發(fā)控制的主要方法是封鎖,鎖就是在一段時間內(nèi)禁止用戶做某些操作以避免產(chǎn)生數(shù)據(jù)不一致 (二) 鎖的分類◆鎖的類別有兩種分法: 1. 從數(shù)據(jù)庫系統(tǒng)的角度來看:分為獨占鎖(即排它鎖),共享鎖和更新鎖
MS-SQL Server 使用以下資源鎖模式。 鎖模式 描述 共享 (S) 用于不更改或不更新數(shù)據(jù)的操作(只讀操作),如 SELECT 語句。 更新 (U) 用于可更新的資源中。防止當(dāng)多個會話在讀取、鎖定以及隨后可能進行的資源更新時發(fā)生常見形式的死鎖。 排它 (X) 用于數(shù)據(jù)修改操作,例如 INSERT、UPDATE 或 DELETE。確保不會同時同一資源進行多重更新。 意向鎖 用于建立鎖的層次結(jié)構(gòu)。意向鎖的類型為:意向共享 (IS)、意向排它 (IX) 以及與意向排它共享 (SIX)。 架構(gòu)鎖 在執(zhí)行依賴于表架構(gòu)的操作時使用。架構(gòu)鎖的類型為:架構(gòu)修改 (Sch-M) 和架構(gòu)穩(wěn)定性 (Sch-S)。 大容量更新 (BU) 向表中大容量復(fù)制數(shù)據(jù)并指定了 TABLOCK 提示時使用。 ◆共享鎖
共享 (S) 鎖允許并發(fā)事務(wù)讀取 (SELECT) 一個資源。資源上存在共享 (S) 鎖時,任何其它事務(wù)都不能修改數(shù)據(jù)。一旦已經(jīng)讀取數(shù)據(jù),便立即釋放資源上的共享 (S) 鎖,除非將事務(wù)隔離級別設(shè)置為可重復(fù)讀或更高級別,或者在事務(wù)生存周期內(nèi)用鎖定提示保留共享 (S) 鎖。 ◆更新鎖
更新 (U) 鎖可以防止通常形式的死鎖。一般更新模式由一個事務(wù)組成,此事務(wù)讀取記錄,獲取資源(頁或行)的共享 (S) 鎖,然后修改行,此操作要求鎖轉(zhuǎn)換為排它 (X) 鎖。如果兩個事務(wù)獲得了資源上的共享模式鎖,然后試圖同時更新數(shù)據(jù),則一個事務(wù)嘗試將鎖轉(zhuǎn)換為排它 (X) 鎖。共享模式到排它鎖的轉(zhuǎn)換必須等待一段時間,因為一個事務(wù)的排它鎖與其它事務(wù)的共享模式鎖不兼容;發(fā)生鎖等待。第二個事務(wù)試圖獲取排它 (X) 鎖以進行更新。由于兩個事務(wù)都要轉(zhuǎn)換為排它 (X) 鎖,并且每個事務(wù)都等待另一個事務(wù)釋放共享模式鎖,因此發(fā)生死鎖。 若要避免這種潛在的死鎖問題,請使用更新 (U) 鎖。一次只有一個事務(wù)可以獲得資源的更新 (U) 鎖。如果事務(wù)修改資源,則更新 (U) 鎖轉(zhuǎn)換為排它 (X) 鎖。否則,鎖轉(zhuǎn)換為共享鎖。 ◆排它鎖
排它 (X) 鎖可以防止并發(fā)事務(wù)對資源進行訪問。其它事務(wù)不能讀取或修改排它 (X) 鎖鎖定的數(shù)據(jù) ◆意向鎖 意向鎖表示 SQL Server 需要在層次結(jié)構(gòu)中的某些底層資源上獲取共享 (S) 鎖或排它 (X) 鎖。例如,放置在表級的共享意向鎖表示事務(wù)打算在表中的頁或行上放置共享 (S) 鎖。在表級設(shè)置意向鎖可防止另一個事務(wù)隨后在包含那一頁的表上獲取排它 (X) 鎖。意向鎖可以提高性能,因為 SQL Server 僅在表級檢查意向鎖來確定事務(wù)是否可以安全地獲取該表上的鎖。而無須檢查表中的每行或每頁上的鎖以確定事務(wù)是否可以鎖定整個表。 意向鎖包括意向共享 (IS)、意向排它 (IX) 以及與意向排它共享 (SIX)。
鎖模式 描述
意向共享 (IS) 通過在各資源上放置 S 鎖,表明事務(wù)的意向是讀取層次結(jié)構(gòu)中的部分(而不是全部)底層資源。 意向排它 (IX) 通過在各資源上放置 X 鎖,表明事務(wù)的意向是修改層次結(jié)構(gòu)中的部分(而不是全部)底層資源。IX 是 IS 的超集。 與意向排它共享 (SIX) 通過在各資源上放置 IX 鎖,表明事務(wù)的意向是讀取層次結(jié)構(gòu)中的全部底層資源并修改部分(而不是全部)底層資源。允許頂層資源上的并發(fā) IS 鎖。例如,表的 SIX 鎖在表上放置一個 SIX 鎖(允許并發(fā) IS 鎖),在當(dāng)前所修改頁上放置 IX 鎖(在已修改行上放置 X 鎖)。雖然每個資源在一段時間內(nèi)只能有一個 SIX 鎖,以防止其它事務(wù)對資源進行更新,但是其它事務(wù)可以通過獲取表級的 IS 鎖來讀取層次結(jié)構(gòu)中的底層資源。 ◆獨占鎖:
只允許進行鎖定操作的程序使用,其他任何對他的操作均不會被接受。執(zhí)行數(shù)據(jù)更新命令時,SQL Server會自動使用獨占鎖。當(dāng)對象上有其他鎖存在時,無法對其加獨占鎖。 共享鎖:共享鎖鎖定的資源可以被其他用戶讀取,但其他用戶無法修改它,在執(zhí)行Select時,SQL Server會對對象加共享鎖。 ◆更新鎖: 當(dāng)SQL Server準備更新數(shù)據(jù)時,它首先對數(shù)據(jù)對象作更新鎖鎖定,這樣數(shù)據(jù)將不能被修改,但可以讀取。等到SQL Server確定要進行更新數(shù)據(jù)操作時,他會自動將更新鎖換為獨占鎖,當(dāng)對象上有其他鎖存在時,無法對其加更新鎖。 2. 從程序員的角度看:分為樂觀鎖和悲觀鎖。
◆樂觀鎖:完全依靠數(shù)據(jù)庫來管理鎖的工作。 ◆悲觀鎖:程序員自己管理數(shù)據(jù)或?qū)ο笊系逆i處理。 MS-SQLSERVER 使用鎖在多個同時在數(shù)據(jù)庫內(nèi)執(zhí)行修改的用戶間實現(xiàn)悲觀并發(fā)控制
三 鎖的粒度鎖粒度是被封鎖目標的大小,封鎖粒度小則并發(fā)性高,但開銷大,封鎖粒度大則并發(fā)性低但開銷小 SQL Server支持的鎖粒度可以分為為行、頁、鍵、鍵范圍、索引、表或數(shù)據(jù)庫獲取鎖 資源 描述 RID 行標識符。用于單獨鎖定表中的一行。 鍵 索引中的行鎖。用于保護可串行事務(wù)中的鍵范圍。 頁 8 千字節(jié) (KB) 的數(shù)據(jù)頁或索引頁。 擴展盤區(qū) 相鄰的八個數(shù)據(jù)頁或索引頁構(gòu)成的一組。 表 包括所有數(shù)據(jù)和索引在內(nèi)的整個表。 DB 數(shù)據(jù)庫。 SQL Server 提供以下的鎖級別:
DATABASE -- 無論何時當(dāng)一個SQL Server 進程正在使用除master以外的數(shù)據(jù)庫時,Lock Manager為該進程授予數(shù)據(jù)庫級的鎖。數(shù)據(jù)庫級的鎖總是共享鎖,用于跟蹤何時數(shù)據(jù)庫在使用中,以防其他進程刪除該數(shù)據(jù)庫,將數(shù)據(jù)庫置為脫機,或者恢復(fù)數(shù)據(jù)庫。注意,由于master和tempdb數(shù)據(jù)庫不能被刪除或置為脫機,所以不需要在它們之上加鎖。 FILE -- 文件級的鎖用于鎖定數(shù)據(jù)庫文件。 EXTENT -- Extent鎖用于鎖定extents,通常僅在空間分配和重新分配的時候使用。一個extent由8個連續(xù)的數(shù)據(jù)頁或索引頁組成。Extent鎖可以是共享鎖也可以是獨占鎖。 ALLOCATION_UNIT -- 使用在數(shù)據(jù)庫分配單元上。 TABLE -- 這種級別的鎖將鎖定整個表,包括數(shù)據(jù)和索引。何時將獲得表級鎖的例子包括在Serializable隔離級別下從包含大量數(shù)據(jù)的表中選取所有的行,以及在表上執(zhí)行不帶過濾條件的update或delete。 Heap or B-Tree (HOBT) -- 用于堆數(shù)據(jù)頁,或者索引的二叉樹結(jié)構(gòu)。 PAGE -- 使用頁級鎖,由8KB數(shù)據(jù)或者索引信息組成的整個頁被鎖定。當(dāng)需要讀取一頁的所有行或者需要執(zhí)行頁級別的維護如頁拆分后更新頁指針時,將會獲取頁級鎖。 Row ID (RID) -- 使用RID鎖,頁內(nèi)的單一行被鎖定。無論何時當(dāng)提供最大化的資源并發(fā)性訪問是有效并且可能時,將獲得RID鎖。 KEY -- SQL Server使用兩種類型的Key鎖。其中一個的使用取決于當(dāng)前會話的鎖隔離級別。對于運行于Read Committed 或者 Repeatable Read 隔離模式下的事務(wù),SQL Server 鎖定與被訪問的行相關(guān)聯(lián)的的實際索引key。(如果是表的聚集索引,數(shù)據(jù)行位于索引的葉級。行上在這些你看到的是Key鎖而不是行級鎖。)若在Serializable隔離模式下,通過鎖定一定范圍的key值從而不允許新的行插入到該范圍內(nèi),SQL Server防止了“幻讀”。這些鎖因而被稱作“key-range
lock”。 METADATA -- 用于鎖定系統(tǒng)目錄信息(元數(shù)據(jù))。 APPLICATION -- 允許用戶定義他們自己的鎖,指定資源名稱、鎖模式、所有者、timeout間隔。
四 SQL Server 鎖類型(與粒度相對應(yīng)) 1. HOLDLOCK: 在該表上保持共享鎖,直到整個事務(wù)結(jié)束,而不是在語句執(zhí)行完立即釋放所添加的鎖。 2. NOLOCK:不添加共享鎖和排它鎖,當(dāng)這個選項生效后,可能讀到未提交讀的數(shù)據(jù)或“臟數(shù)據(jù)”,這個選項僅僅應(yīng)用于SELECT語句?! ?nbsp; 3. PAGLOCK:指定添加頁鎖(否則通??赡芴砑颖礞i)?!?nbsp; 4. READCOMMITTED用與運行在提交讀隔離級別的事務(wù)相同的鎖語義執(zhí)行掃描。默認情況下,SQL Server 2000 在此隔離級別上操作。 5. READPAST: 跳過已經(jīng)加鎖的數(shù)據(jù)行,這個選項將使事務(wù)讀取數(shù)據(jù)時跳過那些已經(jīng)被其他事務(wù)鎖定的數(shù)據(jù)行,而不是阻塞直到其他事務(wù)釋放鎖,READPAST僅僅應(yīng)用于READ COMMITTED隔離性級別下事務(wù)操作中的SELECT語句操作?! ?br/>6. READUNCOMMITTED:等同于NOLOCK?! ?nbsp; 7. REPEATABLEREAD:設(shè)置事務(wù)為可重復(fù)讀隔離性級別?!?nbsp; 8. ROWLOCK:使用行級鎖,而不使用粒度更粗的頁級鎖和表級鎖?! ?nbsp; 9. SERIALIZABLE:用與運行在可串行讀隔離級別的事務(wù)相同的鎖語義執(zhí)行掃描。等同于 HOLDLOCK。 10. TABLOCK:指定使用表級鎖,而不是使用行級或頁面級的鎖,SQL Server在該語句執(zhí)行完后釋放這個鎖,而如果同時指定了HOLDLOCK,該鎖一直保持到這個事務(wù)結(jié)束?! ?br/>11. TABLOCKX:指定在表上使用排它鎖,這個鎖可以阻止其他事務(wù)讀或更新這個表的數(shù)據(jù),直到這個語句或整個事務(wù)結(jié)束?!?nbsp; 12. UPDLOCK :指定在讀表中數(shù)據(jù)時設(shè)置更新 鎖(update lock)而不是設(shè)置共享鎖,該鎖一直保持到這個語句或整個事務(wù)結(jié)束,使用UPDLOCK的作用是允許用戶先讀取數(shù)據(jù)(而且不阻塞其他用戶讀數(shù)據(jù)),并且保證在后來再更新數(shù)據(jù)時,這一段時間內(nèi)這些數(shù)據(jù)沒有被其他用戶修改。
五 鎖定時間的長短 鎖保持的時間長度為保護所請求級別上的資源所需的時間長度。
用于保護讀取操作的共享鎖的保持時間取決于事務(wù)隔離級別。采用 READ COMMITTED 的默認事務(wù)隔離級別時,只在讀取頁的期間內(nèi)控制共享鎖。在掃描中,直到在掃描內(nèi)的下一頁上獲取鎖時才釋放鎖。如果指定 HOLDLOCK 提示或者將事務(wù)隔離級別設(shè)置為 REPEATABLE READ 或 SERIALIZABLE,則直到事務(wù)結(jié)束才釋放鎖。
根據(jù)為游標設(shè)置的并發(fā)選項,游標可以獲取共享模式的滾動鎖以保護提取。當(dāng)需要滾動鎖時,直到下一次提取或關(guān)閉游標(以先發(fā)生者為準)時才釋放滾動鎖。但是,如果指定 HOLDLOCK,則直到事務(wù)結(jié)束才釋放滾動鎖。
用于保護更新的排它鎖將直到事務(wù)結(jié)束才釋放。
如果一個連接試圖獲取一個鎖,而該鎖與另一個連接所控制的鎖沖突,則試圖獲取鎖的連接將一直阻塞到: 將沖突鎖釋放而且連接獲取了所請求的鎖。 連接的超時間隔已到期。默認情況下沒有超時間隔,但是一些應(yīng)用程序設(shè)置超時間隔以防止無限期等待 六 SQL Server 中鎖的自定義 ◆處理死鎖和設(shè)置死鎖優(yōu)先級
死鎖就是多個用戶申請不同封鎖,由于申請者均擁有一部分封鎖權(quán)而又等待其他用戶擁有的部分封鎖而引起的無休止的等待
可以使用SET DEADLOCK_PRIORITY控制在發(fā)生死鎖情況時會話的反應(yīng)方式。如果兩個進程都鎖定數(shù)據(jù),并且直到其它進程釋放自己的鎖時,每個進程才能釋放自己的鎖,即發(fā)生死鎖情況。
◆2 處理超時和設(shè)置鎖超時持續(xù)時間。
@@LOCK_TIMEOUT 返回當(dāng)前會話的當(dāng)前鎖超時設(shè)置,單位為毫秒
SET LOCK_TIMEOUT 設(shè)置允許應(yīng)用程序設(shè)置語句等待阻塞資源的最長時間。當(dāng)語句等待的時間大于 LOCK_TIMEOUT 設(shè)置時,系統(tǒng)將自動取消阻塞的語句,并給應(yīng)用程序返回"已超過了鎖請求超時時段"的 1222 號錯誤信息
示例
下例將鎖超時期限設(shè)置為 1,800 毫秒。 SET LOCK_TIMEOUT 1800 ◆設(shè)置事務(wù)隔離級別。
◆對 SELECT、INSERT、UPDATE 和 DELETE 語句使用表級鎖定提示。
◆配置索引的鎖定粒度
可以使用 sp_indexoption 系統(tǒng)存儲過程來設(shè)置用于索引的鎖定粒度 七 查看鎖的信息 1 執(zhí)行 EXEC SP_LOCK 報告有關(guān)鎖的信息
2 查詢分析器中按Ctrl+2可以看到鎖的信息 八 使用注意事項 如何避免死鎖,最小化鎖競爭
1 使用事務(wù)時,盡量縮短事務(wù)的邏輯處理過程,及早提交或回滾事務(wù),事務(wù)持有鎖的時間越短,鎖競爭發(fā)生的機會就越少;將不是事務(wù)所管理的工作單元鎖必需的命令移出事務(wù)。; 2 設(shè)置死鎖超時參數(shù)為合理范圍,如:3分鐘-10分種;超過時間,自動放棄本次操作,避免進程懸掛; 3 優(yōu)化程序,檢查并避免死鎖現(xiàn)象出現(xiàn); 4 .對所有的腳本和SP都要仔細測試,在正是版本之前。 5 所有的SP都要有錯誤處理(通過@error) 6 一般不要修改SQL SERVER事務(wù)的默認級別。不推薦強行加鎖 7 將組成事務(wù)的語句作為一個的單獨的批命令處理,以消除 BEGIN TRAN 和 COMMIT TRAN 語句之間的網(wǎng)絡(luò)延遲造成的不必要的延遲。 8 考慮完全地使用存儲過程編寫事務(wù)代碼。典型地,存儲過程比批命令運行更快。 9 在游標中盡可早地Commit更新。因為游標處理比面向集合的處理慢得多,因此導(dǎo)致鎖被持有的時間更久。
10 使用每個進程所需的最低級別的鎖隔離。比如說,如果臟讀是可接受的并且不要求結(jié)果必須精確,那么可以考慮使用事務(wù)隔離級別0(Read Uncommitted),僅在絕對必要時才使用Repeatable Read or Serializable隔離級別。
11 在 BEGIN TRAN 和 COMMIT TRAN 語句之間,絕不允許用戶交互,因為這樣做可能鎖被持有無限期的時間。
九 幾個有關(guān)鎖的問題1 如何鎖一個表的某一行 SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED SELECT * FROM table ROWLOCK WHERE id = 1 2 鎖定數(shù)據(jù)庫的一個表 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解鎖 ◆排它鎖 新建兩個連接,在第一個連接中執(zhí)行以下語句 update table1 set A='aa' where B='b2' waitfor delay '00:00:30' --等待30秒 select * from table1 where B='b2'
若同時執(zhí)行上述兩個語句,則select查詢必須等待update執(zhí)行完畢才能執(zhí)行即要等待30秒 ◆共享鎖
在第一個連接中執(zhí)行以下語句 select * from table1 holdlock --holdlock人為加鎖 waitfor delay '00:00:30' --等待30秒
◆共享鎖 在第一個連接中執(zhí)行以下語句 select * from table1 holdlock --holdlock人為加鎖 waitfor delay '00:00:30' --等待30秒
在第二個連接中執(zhí)行以下語句 select A,C from table1 where B='b2' update table1 set A='aa' where B='b2'
若同時執(zhí)行上述兩個語句,則第二個連接中的select查詢可以執(zhí)行 而update必須等待第一個事務(wù)釋放共享鎖轉(zhuǎn)為排它鎖后才能執(zhí)行 即要等待30秒 ◆死鎖 update table1 set A='aa' where B='b2' update table2 set D='d5' where E='e1' update table2 set D='d5' where E='e1' update table1 set A='aa' where B='b2'
同時執(zhí)行,系統(tǒng)會檢測出死鎖,并中止進程 十 應(yīng)用程序鎖:
應(yīng)用程序鎖就是客戶端代碼生成的鎖,而不是sql server本身生成的鎖 處理應(yīng)用程序鎖的兩個過程 sp_getapplock 鎖定應(yīng)用程序資源 sp_releaseapplock 為應(yīng)用程序資源解鎖 注意: 鎖定數(shù)據(jù)庫的一個表的區(qū)別 SELECT * FROM table WITH (HOLDLOCK) 其他事務(wù)可以讀取表,但不能更新刪除 SELECT * FROM table WITH (TABLOCKX) 其他事務(wù)不能讀取表,更新和刪除 交讀事務(wù)使用行版本控制。 使用快照隔離。 使用綁定連接。 二 鎖的分析及應(yīng)用系列
1 用SqlServer Profile來查看分析鎖的信息 這個工具我想大家都明白,它的監(jiān)視能力真的是無所不能。。。鎖的痙攣狀態(tài)也全在它的掌握之中。 1. 首先我做一個Person表,Name字段設(shè)定4000字節(jié),這樣一個數(shù)據(jù)頁可以容納2條數(shù)據(jù),如下圖: DROP TABLE dbo.PersonCREATE TABLE Person(ID INT IDENTITY,NAME CHAR(4000) DEFAULT 'aaaaa')--插入6條,生成3個數(shù)據(jù)頁INSERT INTO dbo.Person DEFAULT VALUESgo 6 2. 下面我們看看數(shù)據(jù)在數(shù)據(jù)頁的分布情況。 
3. 然后我們開啟Profile,在“事件選擇”的Events中選擇”Lock:Acquired“和”Lock:Released“ ,然后運行,如下圖: 
使用測試數(shù)據(jù) 1. 首先我執(zhí)行一個簡單的 SELECT * FROM dbo.Person,看看表/數(shù)據(jù)頁/記錄的加鎖情況。 
從圖中可以看到,select執(zhí)行的大概步驟如下: 第一步:給表(Object)加上IS(意向共享鎖)。 第二步:先給1:78號數(shù)據(jù)頁加IS鎖,掃描78號數(shù)據(jù)頁,然后釋放IS鎖。 第三步:同樣的道理掃描之后的數(shù)據(jù)頁。 第四步:最后釋放表的IS鎖,結(jié)束整個鎖流程。 看完上面的一系列的Lock:Acquired 和 Lock:Released的話,你有沒有發(fā)現(xiàn)一個問題,不是說好給記錄(RID)加上S鎖么???這里沒加,是因為引擎進入78號數(shù)據(jù)頁的時候,未發(fā)現(xiàn)它存在IU鎖或者IX鎖。。。所以。。。這個屬于鎖的組合,后續(xù)會說。 2. 接下來用UPDATE dbo.Person SET NAME='bbbbb' WHERE ID=3來看看update的整個過程,乍一看,Profile捕獲到的記錄還是比較多的,下面具體看圖: 
第一步: 給表(Object)加上IX鎖, 第二步: 給數(shù)據(jù)頁(1:78)數(shù)據(jù)頁分配IU鎖。然后開始逐一掃描78號數(shù)據(jù)頁的RID記錄,進入前就Acquired,退出后就Released,當(dāng)掃描完78號數(shù)據(jù)頁的所有RID后,再釋放78 號數(shù)據(jù)頁的IU鎖,進入下一個數(shù)據(jù)頁。。。 第三步: 我們發(fā)現(xiàn)ID=3是在89號數(shù)據(jù)頁上,當(dāng)引擎掃到該RID之后,我們觀察到89號的數(shù)據(jù)頁由IU鎖變成了IX鎖,并且把1:89:0(slot為0的記錄)由U鎖變成X鎖,變成X鎖 后,就排斥了其他所有的鎖,這時候就可以進行Update操作了。 第四步: 后面就繼續(xù)90號數(shù)據(jù)頁,步驟類似,第二步和第三步。 不知道細心的你有沒有發(fā)現(xiàn),在Released Object之前我們才釋放1:89:0的X鎖,然后釋放89號數(shù)據(jù)頁的IX鎖,這說明什么???說明這個Update是貫穿于這個事務(wù)的,不像Select操作中,掃完一個數(shù)據(jù)頁就釋放一個數(shù)據(jù)頁。 3. 最后再看一個DELETE FROM dbo.Person WHERE ID=3 的操作。 
大概掃了一下上面的圖,或許你感覺和Update操作大差不差,會掃描數(shù)據(jù)頁中的每個記錄并加上U鎖。當(dāng)在1:89:0槽位中找到了目標記錄后,然后將U鎖轉(zhuǎn)化為X鎖,具體可以參考Update。 2 深入的探討鎖機制 上一篇我只是做了一個堆表讓大家初步的認識到鎖的痙攣狀態(tài),但是在現(xiàn)實世界上并沒有這么簡單的事情,起碼我的表不會沒有索引對吧,,,還有就是我的表一定會有很多的連接過來,10:1的讀寫,很多碼農(nóng)可能都會遇到類似神乎其神的死鎖,卡住,讀不出來,插不進入等等神仙的事情導(dǎo)致性能低下,這篇我們一起來探討下。 一: 當(dāng)select遇到性能低下的update會怎么樣? 1. 還是使用原始的person表,插入6條數(shù)據(jù),由于是4000字節(jié),所以兩條數(shù)據(jù)就是一個數(shù)據(jù)頁,如下圖: 1 DROP TABLE dbo.Person2 CREATE TABLE Person(ID INT IDENTITY,NAME CHAR(4000) DEFAULT 'aaaaa')3 --插入6條數(shù)據(jù),剛好3個數(shù)據(jù)頁4 INSERT INTO dbo.Person DEFAULT VALUES5 go 6 2. 為了模擬性能低下的Update操作,我們開個顯式事務(wù)來更新ID=4的記錄,并且用profile看一下,如下圖: 1 BEGIN TRAN2 UPDATE dbo.Person SET NAME='bbbbb' WHERE id=4 
3. 然后我們開下另一個會話連接,讀取ID=6的記錄會是怎樣?好奇嗎? 1 SELECT * FROM Person WHERE ID=6 
從上面流程你是否看到,當(dāng)掃描到89號數(shù)據(jù)頁的slot1槽位的時候卡住了。。。我想你應(yīng)該知道update正好已經(jīng)給這條記錄加上了X鎖。。。如果你夠細心,你還會發(fā)現(xiàn),給S鎖附加記錄的條件是在當(dāng)引擎發(fā)現(xiàn)記錄所在的數(shù)據(jù)頁已經(jīng)附加上了IX鎖的情況下,才給該號數(shù)據(jù)頁下的每條記錄附加S鎖,對吧。。。好了,既然在Profile上面看不到了,我還是有其他辦法來判斷到底select語句現(xiàn)在處于什么狀態(tài)。 4. 使用sys.dm_tran_locks來看當(dāng)前各個連接持有鎖的狀態(tài)。 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 l6 LEFT JOIN sys.partitions AS p7 ON l.resource_associated_entity_id=p.hobt_id 
仔細觀察上圖可以看到,當(dāng)前有51和52號會話,51號在1:89:1槽位上使用了X鎖并且沒有釋放,52號此時也進入了1:89:1中,并且想給該RowID附加S鎖,但是你也知道S和X鎖是排斥的,所以很無奈的一直保持等待狀態(tài)。 二:使用索引或許可以幫你逃過一劫 當(dāng)你看完上面的講敘,是不是有點害怕???要是在生產(chǎn)環(huán)境下出現(xiàn)了這種情況,那我們是不是死的很慘???那接下來使用索引是不是真的可以幫我們躲過一劫呢?下面跟我一起看一看。 1. 新建索引index 1 -- 在ID列上建一個index2 CREATE INDEX idx_person ON dbo.Person(ID) 2. 然后我們看下數(shù)據(jù)頁的分布情況,可以看到下圖中78,89,90是表數(shù)據(jù)頁,93號為索引數(shù)據(jù)頁。 1 DBCC TRACEON(2588,3604)2 DBCC IND(Ctrip,Person,-1) 
3. 麻蛋的,繼續(xù)執(zhí)行上面的那個慢update BEGIN TRANUPDATE dbo.Person SET NAME='bbbbb' WHERE id=4 4. 激動人心的時刻來了,由于數(shù)據(jù)太少,所以我這里強制讓引擎執(zhí)行我創(chuàng)建的索引,看看結(jié)果怎樣? 
居然沒卡住???現(xiàn)在是不是有一股強烈的好奇心來了,狗狗狗。。。馬上開啟profile,看看到底都發(fā)生了什么? 
仔細看完這個圖,是不是覺得很有意思呢???具體步驟如下: 第一步:給表(Object)加上IS鎖。 第二步:因為要走索引,給93號索引數(shù)據(jù)頁加上IS鎖。 第三步:找到93號索引數(shù)據(jù)頁的目標key,給這個key加上S鎖,有人可能就會問了。。。這個key不就是6嘛,為什么這個key=(61005a25560e),你要是太好奇我可以告 訴你,年輕人說話不要太屌,每行索引記錄都有一個散列值,這個值就是根據(jù)索引的幾個字段散列出來的,好處就是防止你的索引長度過大,導(dǎo)致鎖這個記錄的
時候太耗費鎖空間了。。。。如果你還是不太相信的話,我用DBCC給你看一看?! ?/p>  第四步:根據(jù)這個key直接跳到存放記錄的90號數(shù)據(jù)頁中,萬幸的是update的記錄剛好不在90號數(shù)據(jù)頁中。。。。就這樣躲過一劫了。。。然后select順利的讀取到了該 讀的記錄,最后釋放相關(guān)的IS鎖。 3 nolock引發(fā)的三級事件的一些思考 曾今有件事情讓我記憶猶新,那年剛來攜程不久,馬上就被安排寫一個接口,供企鵝公司調(diào)用他們員工的差旅信息,然后我就三下五除二的給寫好了,上線之后,大概過了一個月。。。DBA那邊報告數(shù)據(jù)庫出現(xiàn)大量鎖超時,并且及時根據(jù)sql的來源將email發(fā)到了我們部門,指出sql讀取時間過長,并且缺少nolock,影響了大量機票訂單入庫,然后我就拿著sql去生產(chǎn)環(huán)境跑了下,22s。。。花擦。。。項目上線時間太久,版本已經(jīng)不存在了,無法回滾。。。原本準備撤下接口。。??戳讼鲁废陆涌诟由蟦olock時間相差不多,最后決定先加上nolock,發(fā)布緊急單。。。然后再優(yōu)化,DBA那邊暫時做手工解鎖,發(fā)上去后,最后就是損失XXXX訂單。。。定級為三級事件。然后就是追責(zé),當(dāng)然這個責(zé)任只能有老大們?nèi)コ袚?dān)了,出了這次由我引發(fā)的事件,我得思考了,出了事情對我不見得全是壞事,起碼這次會讓我銘記如心,想想也搓,來攜程之前根本就不會關(guān)注要不要給select指定nolock,這其中也包括自己沒遇到過大數(shù)據(jù)吧,也包括自己的能力有限,只知道有鎖這個玩意,細說的話就啥也不知道了,后來才知道攜程有個規(guī)則,就是很多業(yè)務(wù)產(chǎn)線所寫的select都必須指定nolock,懂一點的人可能會說nolock可以提升性能,如果你這樣說,確實是這樣,因為數(shù)據(jù)庫的鎖是有96字節(jié)開銷的,沒了鎖,也就沒有你在profile中看到accquired和released痙攣了,當(dāng)你看完我的事件之后,你可能會意識到,性能提升不是最關(guān)心的,最關(guān)心就是不要出現(xiàn)死鎖,鎖等待。。。好了,言歸正傳,下面我們看看到底在數(shù)據(jù)庫中可以指定多少個鎖??? 一:到底可以指定多少個鎖 這個問題有意思,我們不需要記,只要你裝一個SQL Prompt,有了這個神器,你就知道到底有多少個?如下圖: 1 DROP TABLE dbo.Person2 CREATE TABLE Person(ID INT IDENTITY,NAME CHAR(4000) DEFAULT 'xxxxx')3 INSERT INTO dbo.Person DEFAULT VALUES4 go 6 
一眼掃下去,還是蠻多的,不過你要注意了,那些所謂的XXXLock才是我們需要關(guān)注的,根據(jù)上面的圖,我們大概把鎖分個類。。。 粒度鎖:PAGLOCK, TABLOCK, TABLOCKX, ROWLOCK, NOLOCK 模式鎖:HOLDLOCK, UPDLOCK, XLOCK 接下來我從粒度鎖說起: 1. NOLOCK 都說nolock是無鎖模式的,那到底是怎樣的無鎖呢???到這篇為止,你應(yīng)該知道,如果不加nolock,我們的表,數(shù)據(jù)頁是附加IS鎖的,那接下來我用profile看下兩者有什么區(qū)別?!?/p>
從上圖中,你會看到加上nolock之后,object上面附加了Sch-S鎖,這個鎖叫做“架構(gòu)穩(wěn)定鎖”,很簡單就是sql編譯時附加的一把鎖,目的就是防止在編譯時,有其他連接修改表結(jié)構(gòu),而這個鎖只與Sch-M鎖沖突,與其他鎖都兼容,這說明什么?說明其他連接鎖住了記錄也沒關(guān)系,我的nolock不跟他們打交道,這樣的話,就可能會讀到臟數(shù)據(jù),不過沒關(guān)系,攜程的很多業(yè)務(wù)是容許臟數(shù)據(jù)的,畢竟比鎖等待,死鎖要強得多,再說nolock讀到了其他連接未修改或者未提交的數(shù)據(jù),這個概率也比較低,就算遇到了也沒關(guān)系,一般不會招來客訴的,客人或許再刷下頁面,數(shù)據(jù)或許就正確了,對不對。。。 2.TABLOCK 這個還是比較見名識義的,就是附加在table上的鎖,也就是表鎖了,很恐怖的。。。下面我舉個Update的例子,看看前后對比。 

在上面你有沒有看到,X鎖已經(jīng)附加到OBJECT上面去了。。。這樣的話,其他連接就動不了這個Object了,只能等待。。。 3. PAGLOCK 看了名字你應(yīng)該也知道,就是附加到頁面這個級別的鎖,我也舉一個Update的例子。
1 BEGIN TRAN2 UPDATE dbo.Person SET NAME='aaaaa' WHERE ID=63 4 BEGIN TRAN5 UPDATE dbo.Person WITH(PAGLOCK) SET NAME='bbbbb' WHERE ID=4 

從上面兩個圖中,你應(yīng)該可以看到,原來附加到RID上面的U鎖,由于PagLock的提升,現(xiàn)在要附加到Page上面了,這個就是所謂的數(shù)據(jù)頁鎖。 4.TABLOCKX, ROWLOCK 這兩個我就不細說了,TABLOCKX就是直接附加在table上的X鎖,你可以通過select看一下。 
ROWLOCK的話,默認情況下就是ROWLOCK,比如默認的Update,你會發(fā)現(xiàn)RID上被附加的U鎖,這個就是行鎖。 5.UPDLOCK 這個鎖還是蠻有意思的,它就是update鎖,如果你select下,它會呈現(xiàn)update的鎖痙攣效果。 
6. XLOCK 知道了UPDLOCK鎖,我想XLOCK你也應(yīng)該明白了。。。它就是delete鎖,即排他鎖,我可以讓select帶上排他鎖。 
7.HOLDLOCK 最后一個我也沒鬧明白,據(jù)說是讓語句在整個事務(wù)中持有鎖,然后我就用select和update調(diào)試一下。 1 SELECT * FROM dbo.Person(HOLDLOCK)2 UPDATE dbo.Person WITH(HOLDLOCK) SET NAME='bbbbb' WHERE ID=4 
三 SQL Server 鎖機制 悲觀鎖 樂觀鎖 實測解析 在使用SQL時,大都會遇到這樣的問題,你Update一條記錄時,需要通過Select來檢索出其值或條件,然后在通過這個值來執(zhí)行修改操作。
但當(dāng)以上操作放到多線程中并發(fā)處理時會出現(xiàn)問題:某線程select了一條記錄但還沒來得及update時,另一個線程仍然可能會進來select到同一條記錄。
一般解決辦法就是使用鎖和事物的聯(lián)合機制:
1. 把select放在事務(wù)中, 否則select完成, 鎖就釋放了 2. 要阻止另一個select , 則要手工加鎖, select 默認是共享鎖, select之間的共享鎖是不沖突的, 所以, 如果只是共享鎖, 即使鎖沒有釋放, 另一個select一樣可以下共享鎖, 從而select出數(shù)據(jù)
SELECT * FROM Table WITH(UPDLOCK) --或者 SELECT * FROM Table WITH(TABLOCKX, READPAST) 具體情況而定。
所有Select加 With (NoLock)解決阻塞死鎖,在查詢語句中使用 NOLOCK 和 READPAST 處理一個數(shù)據(jù)庫死鎖的異常時候,其中一個建議就是使用 NOLOCK 或者 READPAST 。有關(guān) NOLOCK 和 READPAST的一些技術(shù)知識點: 對于非銀行等嚴格要求事務(wù)的行業(yè),搜索記錄中出現(xiàn)或者不出現(xiàn)某條記錄,都是在可容忍范圍內(nèi),所以碰到死鎖,應(yīng)該首先考慮,我們業(yè)務(wù)邏輯是否能容忍出現(xiàn)或者不出現(xiàn)某些記錄,而不是尋求對雙方都加鎖條件下如何解鎖的問題。 NOLOCK 和 READPAST 都是處理查詢、插入、刪除等操作時候,如何應(yīng)對鎖住的數(shù)據(jù)記錄。但是這時候一定要注意NOLOCK 和 READPAST的局限性,確認你的業(yè)務(wù)邏輯可以容忍這些記錄的出現(xiàn)或者不出現(xiàn): 簡單來說:
1.NOLOCK 可能把沒有提交事務(wù)的數(shù)據(jù)也顯示出來 2.READPAST 會把被鎖住的行不顯示出來
不使用 NOLOCK 和 READPAST ,在 Select 操作時候則有可能報錯誤:事務(wù)(進程 ID **)與另一個進程被死鎖在 鎖 資源上,并且已被選作死鎖犧牲品。
SELECT * FROM Table WITH(NOLOCK) SELECT * FROM Table WITH(READPAST)
實際開始動手用代碼說話吧!
SQLServer2012在查詢分析器里面開兩個連接
插入鎖: 結(jié)論:“表鎖”鎖定對該表的Select、Update、Delete操作,但不影響對該表的Insert操作也不影響以主鍵Id為條件的Select,所以Select如果不想等待就要在Select后加With(Nolock),但這樣會產(chǎn)生臟數(shù)據(jù)就是其他事務(wù)已更新但并沒有提交的數(shù)據(jù),如果該事務(wù)進行了RollBack則取出的數(shù)據(jù)就是錯誤的,所以好自己權(quán)衡利弊,一般情況下90%以上的Select都允許臟讀,只有賬戶金額相關(guān)的不允許。
------------------A連接 Insert Lock------------------- ( Name, Age, Mobile, AddTime, Type ) VALUES ( 'eee', -- Name - varchar(50) '555', -- Mobile - char(11) GETDATE(), -- AddTime - datetime SELECT resource_type, request_mode,COUNT(*) FROM sys.dm_tran_locks WHERE request_session_id=@@SPID GROUP BY resource_type,request_mode ------------------------B連接 Insert Lock------------------------ ( Name, Age, Mobile, AddTime, Type ) VALUES ( 'fff', -- Name - varchar(50) '123', -- Mobile - char(11) GETDATE(), -- AddTime - datetime SELECT * FROM dbo.UserInfo --需要等待解鎖 SELECT * FROM dbo.UserInfo WHERE Age=1 --需要等待解鎖 SELECT * FROM dbo.UserInfo WHERE Id=3 --可以執(zhí)行查詢(根據(jù)主鍵可以) SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以執(zhí)行查詢(在一個事務(wù)中,有更新字段但還沒有提交,此時就會查處臟數(shù)據(jù)) SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Age=1 --可以執(zhí)行查詢 UPDATE dbo.UserInfo SET Type=5 WHERE Name='fff' --需要等待解鎖 DELETE FROM dbo.UserInfo WHERE Name='fff' --需要等待解鎖
更新鎖: 結(jié)論:“表鎖”鎖定對該表的Select、Update、Delete操作,但不影響對該表的Insert操作也不影響以主鍵Id為條件的Select
-----------------------A連接 Update Lock----------------------- 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 ------------------------B連接 Update Lock------------------------ ( Name, Age, Mobile, AddTime, Type ) VALUES ( 'ppp', -- Name - varchar(50) '666', -- Mobile - char(11) GETDATE(), -- AddTime - datetime SELECT * FROM dbo.UserInfo --需要等待解鎖 SELECT * FROM dbo.UserInfo WHERE Name='ppp' --需要等待解鎖 SELECT * FROM dbo.UserInfo WHERE Id=3 --可以執(zhí)行查詢(根據(jù)主鍵可以) SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以執(zhí)行查詢(在一個事務(wù)中,有更新字段但還沒有提交,此時就會查處臟數(shù)據(jù)) SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Name = 'ppp' --可以執(zhí)行查詢 UPDATE dbo.UserInfo SET Age=8 WHERE Name='ccc' --需要等待解鎖 DELETE dbo.UserInfo WHERE Age = 5 --需要等待解鎖
主鍵鎖: 結(jié)論:“行鎖+表鎖” 鎖定對該表的Select、Update、Delete操作,但不影響對該表的Insert操作也不影響以主鍵Id為條件的Select、Update、Delete
------------------------A連接 Key Lock-------------------- 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 ------------------------B連接 Key Lock---------------------- ( Name, Age, Mobile, AddTime, Type ) VALUES ( 'kkk', -- Name - varchar(50) '234', -- Mobile - char(11) GETDATE(), -- AddTime - datetime SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以執(zhí)行查詢(在一個事務(wù)中,有更新字段但還沒有提交,此時就會查處臟數(shù)據(jù)) SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Name = 'kkk' --可以執(zhí)行查詢 SELECT * FROM dbo.UserInfo --需要等待解鎖 SELECT * FROM dbo.UserInfo WHERE Id=3 --需要等待解鎖(根據(jù)主鍵,但與A連接操作相同行不可) UPDATE dbo.UserInfo SET Name='mmm' WHERE Id=3 --需要等待解鎖(根據(jù)主鍵,但與A連接操作相同行不可) DELETE dbo.UserInfo WHERE Id=3 --需要等待解鎖(根據(jù)主鍵,但與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 --可以執(zhí)行查詢(根據(jù)主鍵可以) UPDATE dbo.UserInfo SET Name='yyy' WHERE Id=1 --可以執(zhí)行更新(根據(jù)主鍵可以) DELETE dbo.UserInfo WHERE Id=1 --可以執(zhí)行刪除(根據(jù)主鍵可以)
索引鎖: 結(jié)論:“行鎖+表鎖” 鎖定對該表的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) 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 ----------------------B連接 Index Lock------------------- ( Name, Age, Mobile, AddTime, Type ) VALUES ( 'iii', -- Name - varchar(50) '235235235', -- Mobile - char(11) GETDATE(), -- AddTime - datetime SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以執(zhí)行查詢(在一個事物中,有更新字段但還沒有提交,此時就會查處臟數(shù)據(jù)) SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Name = 'kkk' --可以執(zhí)行查詢 SELECT * FROM dbo.UserInfo --需要等待解鎖 SELECT * FROM dbo.UserInfo WHERE Id=4 --需要等待解鎖(根據(jù)主鍵,但與A連接操作相同行不可) UPDATE dbo.UserInfo SET Name='mmm' WHERE Id=4 --需要等待解鎖(根據(jù)主鍵,但與A連接操作相同行不可) DELETE dbo.UserInfo WHERE Id=4 --需要等待解鎖(根據(jù)主鍵,但與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 --可以執(zhí)行更新(根據(jù)主鍵可以) UPDATE dbo.UserInfo SET Name='yyy' WHERE Id=1 --可以執(zhí)行更新(根據(jù)主鍵可以) DELETE dbo.UserInfo WHERE Id=1 --可以執(zhí)行刪除(根據(jù)主鍵可以) SELECT * FROM dbo.UserInfo WHERE Name='aaa' --需要等待解鎖(非主鍵不可) UPDATE dbo.UserInfo SET Name='ooo' WHERE Name='aaa' --可以執(zhí)行更新(根據(jù)索引可以) DELETE dbo.UserInfo WHERE Name='aaa' --可以執(zhí)行刪除(根據(jù)索引可以)
悲觀鎖(更新鎖-人工手動設(shè)置上鎖): 結(jié)論:可以理解為在使用版本控制軟件的時候A遷出了一個文件,并且8i將這個87文件鎖定,B就無法再遷出該文件了,直到A遷入解鎖后才能被其他人遷出。
------------------------A連接 Update Lock(悲觀鎖)--------------------- 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 ---------------------------B連接 Update Lock(悲觀鎖)------------------------- SELECT * FROM dbo.UserInfo --可以執(zhí)行查詢 SELECT * FROM dbo.UserInfo WHERE id=2 --可以執(zhí)行查詢 SELECT * FROM dbo.UserInfo WHERE Name='ooo' --可以執(zhí)行查詢 UPDATE dbo.UserInfo SET Age=3 WHERE id=1 --可以執(zhí)行更新(根據(jù)主鍵可以) UPDATE dbo.UserInfo SET Age=3 WHERE Name='ccc' --需要等待解鎖(非主鍵不可) DELETE dbo.UserInfo WHERE id=1 --可以執(zhí)行更新(根據(jù)主鍵可以) DELETE dbo.UserInfo WHERE name='ccc' --需要等待解鎖(非主鍵不可)
樂觀鎖(人工通過邏輯在數(shù)據(jù)庫中模擬鎖) 結(jié)論:可以理解為同樣在使用版本控制軟件的時候A遷出了一個文件,B也可以遷出該文件,兩個人都可以對此文件進行修改,其中一個人先進行提交的時候,版本并沒有變化所以可以正常提交,另一個后提交的時候,發(fā)現(xiàn)版本增加不對稱了,就提示沖突由用戶來選擇如何進行合并再重新進行提交。
--------------------------A客戶端連接 Lock(樂觀鎖)------------------------ -----------------創(chuàng)建優(yōu)惠券表----------------- Id INT PRIMARY KEY IDENTITY(1,1), Number VARCHAR(50) NOT NULL, 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) --查詢數(shù)據(jù) --UPDATE Coupon SET [User]=NULL, UseTime=NULL, IsFlag=0 --還原數(shù)據(jù) -----------------1、模擬高并發(fā)普通更新----------------- DECLARE @User VARCHAR(50) --模擬要使用優(yōu)惠券的用戶 DECLARE @TempId INT --模擬抽選出來的要使用的優(yōu)惠券 SELECT @TempId=Id FROM dbo.Coupon WHERE IsFlag=0 --高并發(fā)時此語句有可能另外一個該事務(wù)已取出的Id --WAITFOR DELAY '00:00:05' --改用此方式要開兩個SQL Management客戶端 UPDATE dbo.Coupon SET IsFlag=1, [User]=@User, UseTime=GETDATE() WHERE Id=@TempId -----------------2、悲觀鎖解決方案----------------- DECLARE @User VARCHAR(50) --模擬要使用優(yōu)惠券的用戶 DECLARE @TempId INT --模擬抽選出來的要使用的優(yōu)惠券 SELECT @TempId=Id FROM dbo.Coupon WITH(UPDLOCK) WHERE IsFlag=0 --高并發(fā)時此語句會鎖定取出的Id數(shù)據(jù)行 --WAITFOR DELAY '00:00:05' --改用此方式要開兩個SQL Management客戶端 UPDATE dbo.Coupon SET IsFlag=1, [User]=@User, UseTime=GETDATE() WHERE Id=@TempId -----------------3、樂觀鎖解決方案----------------- ALTER TABLE dbo.Coupon ADD RowVer ROWVERSION NOT NULL --增加數(shù)據(jù)行版本戳類型字段(微軟新推薦數(shù)據(jù)字段,該字段每張表只能有一個,會在創(chuàng)建行或更新行時自動進行修改無需人為干涉,該字段不能建立索引及主鍵因為會頻繁修改) DECLARE @User VARCHAR(50) --模擬要使用優(yōu)惠券的用戶 DECLARE @TempId INT --模擬抽選出來的要使用的優(yōu)惠券 DECLARE @RowVer BINARY(8) --抽選出來的優(yōu)惠券的版本(ROWVERSION數(shù)據(jù)類型存儲大小為8字節(jié)) SELECT @TempId=Id, @RowVer=RowVer FROM dbo.Coupon WHERE IsFlag=0 --取出可用的Id及對應(yīng)的版本戳 --WAITFOR DELAY '00:00:05' --改用此方式要開兩個SQL Management客戶端 UPDATE dbo.Coupon SET IsFlag=1, [User]=@User, UseTime=GETDATE() WHERE Id=@TempId AND RowVer=@RowVer PRINT('該數(shù)據(jù)已被其他用戶修改') --------------------------B客戶端連接 Lock(樂觀鎖)------------------------ --此測試需要開兩個SQL Management Studio客戶端,在A客戶端使用WAITFOR DELAY來模擬并發(fā)占用,在B客戶端執(zhí)行與A客戶端相同的SQL腳本即可(注釋掉WAITFOR),所以在此不放相同代碼了。
在樂觀鎖和悲觀鎖之間進行選擇的標準是:沖突的頻率與嚴重性。如果沖突很少,或者沖突的后果不會很嚴重,那么通常情況下應(yīng)該選擇樂觀鎖,因為它能得到更好的并發(fā)性,而且更容易實現(xiàn)。但是,如果沖突的結(jié)果對于用戶來說痛苦的,那么就需要使用悲觀策略。
我認為如果同一張表的并發(fā)很高,但并發(fā)處理同一條數(shù)據(jù)的沖突幾率很低,那就應(yīng)該使用樂觀鎖,反之,如果同一張表的并發(fā)不高,但同時處理同一條數(shù)據(jù)的幾率很高,就應(yīng)該使用悲觀鎖。
四 SQL Server 中WITH (NOLOCK)淺析
概念介紹開發(fā)人員喜歡在SQL腳本中使用WITH(NOLOCK), WITH(NOLOCK)其實是表提示(table_hint)中的一種。它等同于 READUNCOMMITTED 。 具體的功能作用如下所示(摘自MSDN): 1: 指定允許臟讀。不發(fā)布共享鎖來阻止其他事務(wù)修改當(dāng)前事務(wù)讀取的數(shù)據(jù),其他事務(wù)設(shè)置的排他鎖不會阻礙當(dāng)前事務(wù)讀取鎖定數(shù)據(jù)。允許臟讀可能產(chǎn)生較多的并發(fā)操作,但其代價是讀取以后會被其他事務(wù)回滾的數(shù)據(jù)修改。這可能會使您的事務(wù)出錯,向用戶顯示從未提交過的數(shù)據(jù),或者導(dǎo)致用戶兩次看到記錄(或根本看不到記錄)。有關(guān)臟讀、不可重復(fù)讀和幻讀的詳細信息,請參閱并發(fā)影響。 2: READUNCOMMITTED 和 NOLOCK 提示僅適用于數(shù)據(jù)鎖。所有查詢(包括那些帶有 READUNCOMMITTED 和 NOLOCK 提示的查詢)都會在編譯和執(zhí)行過程中獲取 Sch-S(架構(gòu)穩(wěn)定性)鎖。因此,當(dāng)并發(fā)事務(wù)持有表的 Sch-M(架構(gòu)修改)鎖時,將阻塞查詢。例如,數(shù)據(jù)定義語言 (DDL) 操作在修改表的架構(gòu)信息之前獲取 Sch-M 鎖。所有并發(fā)查詢(包括那些使用 READUNCOMMITTED 或 NOLOCK 提示運行的查詢)都會在嘗試獲取
Sch-S 鎖時被阻塞。相反,持有 Sch-S 鎖的查詢將阻塞嘗試獲取 Sch-M 鎖的并發(fā)事務(wù)。有關(guān)鎖行為的詳細信息,請參閱鎖兼容性(數(shù)據(jù)庫引擎)。 3: 不能為通過插入、更新或刪除操作修改過的表指定 READUNCOMMITTED 和 NOLOCK。SQL Server 查詢優(yōu)化器忽略 FROM 子句中應(yīng)用于 UPDATE 或 DELETE 語句的目標表的 READUNCOMMITTED 和 NOLOCK 提示。 功能與缺陷 使用WIHT(NOLOCK)有利也有弊,所以在決定使用之前,你一定需要了解清楚WITH(NOLOCK)的功能和缺陷,看其是否適合你的業(yè)務(wù)需求,不要覺得它能提升性能,稀里糊涂的就使用它。 1:使用WITH(NOLOCK)時查詢不受其它排他鎖阻塞 打開會話窗口1,執(zhí)行下面腳本,不提交也不回滾事務(wù),模擬事務(wù)真在執(zhí)行過程當(dāng)中 BEGIN TRAN UPDATE TEST SET NAME='Timmy' WHERE OBJECT_ID =1; --ROLLBACK 打開會話窗口2,執(zhí)行下面腳本,你會發(fā)現(xiàn)執(zhí)行結(jié)果一直查詢不出來(其實才兩條記錄)。當(dāng)前會話被阻塞了 打開會話窗口3,執(zhí)行下面腳本,查看阻塞情況,你會發(fā)現(xiàn)在會話2被會話1給阻塞了,會話2的等待類型為LCK_M_S:“當(dāng)某任務(wù)正在等待獲取共享鎖時出現(xiàn)” 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,執(zhí)行腳本1前,可以用SELECT @@spid查看會話ID)的鎖信息情況,你會發(fā)現(xiàn)表TEST(ObjId=1893581784)持有的鎖信息如下所示 
打開會話窗口4,執(zhí)行下面腳本.你會發(fā)現(xiàn)查詢結(jié)果很快就出來,會話4并不會被會話1阻塞。 SELECT * FROM TEST WITH(NOLOCK) 從上面模擬的這個小例子可以看出,正是由于加上WITH(NOLOCK)提示后,會話1中事務(wù)設(shè)置的排他鎖不會阻礙當(dāng)前事務(wù)讀取鎖定數(shù)據(jù),所以會話4不會被阻塞,從而提升并發(fā)時查詢性能。 2:WITH(NOLOCK) 不發(fā)布共享鎖來阻止其他事務(wù)修改當(dāng)前事務(wù)讀取的數(shù)據(jù),這個就不舉例子了。 本質(zhì)上WITH(NOLOCK)是通過減少鎖和不受排它鎖影響來減少阻塞,從而提高并發(fā)時的性能。所謂凡事有利也有弊,WITH(NOLOCK)在提升性能的同時,也會產(chǎn)生臟讀現(xiàn)象。 如下所示,表TEST有兩條記錄,我準備更新OBJECT_ID=1的記錄,此時事務(wù)既沒有提交也沒有回滾 
BEGIN TRAN UPDATE TEST SET NAME='Timmy' WHERE OBJECT_ID =1; --ROLLBACK 此時另外一個會話使用WITH(NOLOCK)查到的記錄為未提交的記錄值 
假如由于某種原因,該事務(wù)回滾了,那么我們讀取到的OBJECT_ID=1的記錄就是一條臟數(shù)據(jù)。 臟讀又稱無效數(shù)據(jù)的讀出,是指在數(shù)據(jù)庫訪問中,事務(wù)T1將某一值修改,然后事務(wù)T2讀取該值,此后T1因為某種原因撤銷對該值的修改,這就導(dǎo)致了T2所讀取到的數(shù)據(jù)是無效的。 WITH(NOLOCK)使用場景什么時候可以使用WITH(NOLOCK)? 什么時候不能使用WITH(NOLOCK),這個要視你系統(tǒng)業(yè)務(wù)情況,綜合考慮性能情況與業(yè)務(wù)要求來決定是否使用WITH(NOLOCK), 例如涉及到金融或會計成本之類的系統(tǒng),出現(xiàn)臟讀那是要產(chǎn)生嚴重問題的。關(guān)鍵業(yè)務(wù)系統(tǒng)也要慎重考慮。大體來說一般有下面一些場景可以使用WITH(NOLOCK) 1: 基礎(chǔ)數(shù)據(jù)表,這些表的數(shù)據(jù)很少變更。 2:歷史數(shù)據(jù)表,這些表的數(shù)據(jù)很少變更。 3:業(yè)務(wù)允許臟讀情況出現(xiàn)涉及的表。 4:數(shù)據(jù)量超大的表,出于性能考慮,而允許臟讀。 另外一點就是不要濫用WITH(NOLOCK),我發(fā)現(xiàn)有個奇怪現(xiàn)象,很多開發(fā)知道WITH(NOLOCK),但是有不了解臟讀,習(xí)慣性的使用WITH(NOLOCK)。 WITH(NOLOCK)與 NOLOCK區(qū)別為了搞清楚WITH(NOLOCK)與NOLOCK的區(qū)別,我查了大量的資料,我們先看看下面三個SQL語句有啥區(qū)別 SELECT * FROM TEST NOLOCK SELECT * FROM TEST (NOLOCK); SELECT * FROM TEST WITH(NOLOCK); 上面的問題概括起來也就是說NOLOCK、(NOLOCK)、 WITH(NOLOCK)的區(qū)別: 1: NOLOCK這樣的寫法,其實NOLOCK其實只是別名的作用,而沒有任何實質(zhì)作用。所以不要粗心將(NOLOCK)寫成NOLOCK 2:(NOLOCK)與WITH(NOLOCK)其實功能上是一樣的。(NOLOCK)只是WITH(NOLOCK)的別名,但是在SQL Server 2008及以后版本中,(NOLOCK)不推薦使用了,"不借助 WITH 關(guān)鍵字指定表提示”的寫法已經(jīng)過時了。 具體參見MSDN http://msdn.microsoft.com/zh-cn/library/ms143729%28SQL.100%29.aspx 2.1 至于網(wǎng)上說WITH(NOLOCK)在SQL SERVER 2000不生效,我驗證后發(fā)現(xiàn)完全是個謬論。 2.2 在使用鏈接服務(wù)器的SQL當(dāng)中,(NOLOCK)不會生效,WITH(NOLOCK)才會生效。如下所示 
消息 4122,級別 16,狀態(tài) 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)會不會產(chǎn)生鎖 很多人誤以為使用了WITH(NOLOCK)后,數(shù)據(jù)庫庫不會產(chǎn)生任何鎖。實質(zhì)上,使用了WITH(NOLOCK)后,數(shù)據(jù)庫依然對該表對象生成Sch-S(架構(gòu)穩(wěn)定性)鎖以及DB類型的共享鎖, 如下所示,可以在一個會話中查詢一個大表,然后在另外一個會話中查看鎖信息(也可以使用SQL Profile查看會話鎖信息) 不使用WTIH(NOLOCK) 
使用WITH(NOLOCK) 
從上可以看出使用WITH(NOLOCK)后,數(shù)據(jù)庫并不是不生成相關(guān)鎖。 對比可以發(fā)現(xiàn)使用WITH(NOLOCK)后,數(shù)據(jù)庫只會生成DB類型的共享鎖、以及TAB類型的架構(gòu)穩(wěn)定性鎖. 另外,使用WITH(NOLOCK)并不是說就不會被其它會話阻塞,依然可能會產(chǎn)生Schema Change Blocking 會話1:執(zhí)行下面SQL語句,暫時不提交,模擬事務(wù)正在執(zhí)行 BEGIN TRAN ALTER TABLE TEST ADD Grade VARCHAR(10) ; 會話2:執(zhí)行下面語句,你會發(fā)現(xiàn)會話被阻塞,截圖如下所示。 SELECT * FROM TEST WITH(NOLOCK) 
----- 本文引用了以下網(wǎng)址內(nèi)容: http://www.cnblogs.com/huangxincheng/p/4292320.html http://blog./13651903/viewspace-1091664/ http://www.cnblogs.com/kerrycode/p/3946268.html http://www.cnblogs.com/taiyonghai/p/5674462.html
--- end ---
|