SQL Server經過鎖定資源來保證數據庫的一致性。SQL Server中的鎖不會對行、頁、表或索引等資源有實際影響,它更像一個預訂系統,全部任務在數據庫內預訂某些資源時都遵照它。過多的鎖或長時間持有的鎖會致使阻塞和其餘問題,但鎖自己也可能產生一些問題。
1 解決鎖內存問題
爲了肯定SQL Server中鎖使用的內存量,能夠監視SQL Server中的「鎖內存(KB)」計數器和系統監視器(Perfmon)中的「內存管理」對象。經過設置sp_configure中的鎖選項,能夠修改SQL Server中鎖的內存配額。使用SQLServer:Locks計數器,能夠了解更多關於鎖行爲的細節。
若是系統中的鎖內存消耗完了,SQL Server不能分配更多的鎖內存,session會收到消息1204:
The instance of the SQL Server Database Engine cannot obtain a LOCK resource at this time.
Rerun your statement when there are fewer active users. Ask the database administrator to check the lock and memory configuration for this instance, or to check for long-running transactions.
這個消息說得很清楚:須要增長鎖的內存配額,或者減小系統中鎖的數量。
若是鎖佔用很大的內存,應該首先嚐試找出形成這麼多鎖的根本緣由。例如,多是SQL Server的鎖升級不充分。若是是這樣,就須要修改鎖的配置。一旦修改了鎖動態配置,就影響了鎖升級的行爲,由此可能形成意外的影響。
若是數據庫不須要任何寫訪問,建議將其設置爲只讀的。這會下降系統中產生的鎖的數量。在一個只讀的數據庫中,SQL Server仍會發行數據庫的共享鎖和讀表的意向共享鎖,但行鎖、頁鎖及SERIALIZABLE隔離級別的行鎖,都不會被髮行。例如,對於只在夜間更新的報表數據庫,用戶能夠將在白天對數據庫的查詢設置爲只讀的。這樣作對鎖內存的影響會下降,這也是SQL Server的鎖管理器必須作的。還能夠在同一臺服務器上對只讀數據庫建立數據庫快照,SQL Server不會在數據庫快照上發行共享鎖。
爲了減小鎖內存,一樣建議將讀操做與寫操做分開。一種方法是把報表從一個OLTP系統中分開,經過建立報表服務器和使用事務複製或SQL Server集成服務(SSIS)來爲另外一個用戶查詢讀操做的服務器獲取數據。這會去掉OLTP主服務器的共享鎖。若是數據庫服務器可以支持這種方法,能夠考慮用一個數據庫快照來按期卸載讀操做。在本章後面咱們還能夠看到使用一種基於行版本的快照隔離級別來減小讀數據查詢產生的鎖。
2 鎖超時
默認狀態下,一個被阻塞的查詢會無限地等待一個未被知足的鎖的請求。經過使用LOCK_TIMEOUT設置,能夠指定一個session鎖等待的時間。當鎖超時發生時,session會收到消息1222:
Lock request time out period exceeded.
使用LOCK_TIMEOUT給事務帶來了問題,由於錯誤1222發生後,SQL Server只是退出當前程序語句,而並無停止事務。所以須要在Transact-SQL代碼中使用TRY/CATCH模塊來捕獲1222錯誤。若發生了超時,可能須要回滾事務。若要了解更多內容,能夠參考Inside SQL Server 2005:The Storage Engine(《Microsoft SQL Server 2005:存儲引擎》,電子工業出版社,2007)第8章的「設置鎖超時」。
3 鎖升級
SQL Server常常會鎖定表中獨立的行,尤爲當更新和刪除比較少的行時。但執行大規模更新時,SQL Server選擇表中某行或某頁的鎖進行升級,以更好的使用鎖內存資源。但有時鎖升級會形成阻塞,咱們但願減小鎖升級的數量(參考KB文章323630《如何解決SQL Server會中由鎖升級引發的阻塞問題》,獲取更多詳細信息)。
檢測鎖升級有不少方法,最簡單的方法是使用SQL Trace/Profiler中的Lock:Escalation事件類。當升級發生時,該事件被觸發。但一個升級會有多個觸發,因此將它們綁定在一塊兒很重要。
確保選擇Lock:Escalation事件類的默認列,這些列提供基本信息。但咱們添加如下列可能也頗有用:TransactionID、DatabaseID、DatabaseName和ObjectID。由於可能看到trace中一個升級事件的多行,可使用TransactionID將它們綁定在一塊兒,特定對象(即表)可使用ObjectID。
經過監視表鎖的數目或者和它們的持續時間,能夠檢測到正在發生的升級。若是能夠估計應用系統不多須要(或者曾經須要)表上的共享鎖或獨佔鎖,就能夠推斷不管何時咱們看到這樣的鎖,它都由鎖升級產生。能夠經過sys.dm_tran_locks DMV在給定的時間點探測表鎖。下面的查詢顯示了一個實例:
SELECT
request_session_id,
resource_type,
DB_NAME(resource_database_id) AS DatabaseName,
OBJECT_NAME(resource_associated_entity_id) AS TableName,
request_mode,
request_type,
request_status
FROM sys.dm_tran_locks AS TL
JOIN sys.all_objects AS AO
ON TL.resource_associated_entity_id = AO.object_id
WHERE request_type = ’LOCK’
AND request_status = ’GRANT’
AND request_mode IN (’X’,’S’)
AND AO.type = ’U’
AND resource_type = ’OBJECT’
AND TL.resource_database_id = DB_ID();
上面用來查找表鎖的查詢引用了sys.all_objects的目錄視圖,因此返回信息的範圍限制在查詢運行的數據庫上。因爲sys.dm_tran_locks沒有返回鎖定對象更詳細的信息,就沒有辦法得知這個對象是不是表。這樣一來,就必須加入返回那些信息的數據庫的一些東西,而在這種狀況下,sys.all_objects包含這些信息,並且OBJECT_NAME()函數能夠返回表的名稱。(實例見第1章「性能故障檢修方法」。)可是,它們都只返回當前數據庫的信息。所以,查詢過濾器的最後一個條件限制了當前數據庫中那些資源的返回行。
另外一種策略是使用sp_lock系統存儲過程,它返回鎖類型,從而能夠查看錶鎖。不幸的是,爲了過濾sp_lock,必須抓取臨時數據,而後查詢它並在一個WHERE子句中過濾。能夠從sp_lock存儲過程當中提取key並執行它,可是它只適合於查詢sys.dm_tran_locks DMV並對其過濾。
解決鎖升級
防止多餘的鎖升級的最簡單的方法是減小如下批量操做的批次大小:插入、更新或刪除。例如,若必須執行批量更新,能夠限制行數,或鎖數量的最大值5 000。咱們須要嘗試找到防止升級的合適數值。SQL Server的查詢優化器能夠檢測到表遍歷和任何的鎖升級。
目前,對每一個表防止鎖升級最有趣的方法是在表上建立意向鎖,這樣SQL Server就不能升級鎖。一般鎖升級隻影響不多的查詢或隻影響某個查詢,咱們能夠關注有問題的表。微軟KB文章323630《如何解決SQL Server中由鎖升級引發的阻塞問題》給出了一個很好的例子。對於SQL Server實例數據庫AdventureWorks中的表Sales.SalesOrderDetail,若要防止鎖升級,以下代碼可使這張表在1小時內防止鎖升級:
BEGIN TRAN;
SELECT *
FROM Sales.SalesOrderDetail WITH (UPDLOCK, HOLDLOCK)
WHERE 1=0;
WAITFOR DELAY ’1:00:00’;
COMMIT;
這個查詢能夠防止表Sales.SalesOrderDetail上的鎖升級(雖然事務日誌不會比沒有它增長更多)。當升級嘗試發生時,仍然能夠在SQL Profiler中看到Lock:Escalation事務,可是經過檢查sys.dm_tran_locks,咱們能驗證事務只採起了行鎖。不幸的是,這個請求使表的一個事務無期限地open,即便它並不鎖定任何行。並且,若是這張表有其餘表的外鍵或觸發器,SQL Server也會升級它的鎖,因此阻止一張表的鎖升級不像想象中那麼簡單。
一種有風險的選擇是徹底關掉鎖升級。例如,能夠設置跟蹤標識1211,它禁止整個SQL Server實例的鎖升級。問題是雖然這個選項能夠減小阻塞,可是它會形成更多的鎖,所以鎖內存增長。若是系統的鎖內存消耗完,會使SQL Server中止或下降它的性能。還可使用跟蹤標識1224,它禁止鎖升級,直到鎖管理器使用掉SQL Server實例的40%的非AWE動態分配內存。若是鎖內存的數量達到可用非AWE內存的60%,鎖內存就會被消耗完。
另外一種下降鎖升級的方法是使用查詢的「行鎖」或「頁鎖」提示。這必須在每張表每一個查詢上完成。遇到的問題和全部鎖提示的問題相同:查詢不能使用更優化的計劃。若是指定「頁鎖」提示,SQL Server仍然會在表鎖級別進行鎖升級,並且在其餘不須要升級的狀況下禁止使用行鎖,行鎖會有更好的行爲。
還能夠設置索引選項,經過SQL Server 2005的CREATE/ALTER INDEX語句能夠改善sp_indexoption系統存儲過程的性能。從根本上說,經過SET選項設置ALLOW_ROW_LOCKS或ALLOW_PAGE_LOCKS爲OFF,能夠防止索引上的行鎖或頁鎖。這兩個選項的默認值是ON。這些選項有效地控制索引葉子節點的顆粒度,迫使SQL Server在一個較高的顆粒度起點使用鎖。重申一下,一般來講這些選項沒有很高的價值,由於它們阻止了可能受益於較低鎖顆粒度的查詢計劃。
若是因爲讀操做產生鎖升級,咱們能夠試着去掉數據庫中的讀活動,經過使用數據庫快照,複製到報表數據庫上,或使用一個「快照隔離」級別。