sql server 鎖與事務撥雲見日(上)


 一.概述

  講到sql server鎖管理時,感覺它是一個大話題,因為它不但重要而且涉及的知識點很多,重點在於要掌握高並發要先要掌握鎖與事務,涉及的知識點多它包括各式各樣的鎖,鎖的組合,鎖的排斥,鎖延伸出來的事務隔離級別, 鎖住資源帶來的阻塞,鎖之間的爭用造成的死鎖,索引數據與鎖等。這次介紹鎖和事務,我想分上中下篇,上篇詳細介紹鎖,中篇介紹事務,下篇總結, 針對鎖與事務我想把我掌握的以及參考多方面資料,整合出來盡量說詳細。 最后說下,對於高級開發人員或DBA,鎖與事務應該是重點關注的,它就像是數據庫里的一個大boss,如完全掌握了它,數據庫就會像就像庖丁解牛一樣游刃有余  哈哈 。

二.鎖的產生背景

  在關系型數據庫里鎖是無處不再的。當我們在執行增刪改查的sql語句時,鎖也就產生了。鎖對應的就的是事務,不去顯示加tran就是常說的隱式事務。當我們寫個存儲過程希望數據一致性時, 要么同時回滾,要么同時提交,這時我們用begin tran 來做顯示事務。鎖的范圍就是事務。在sql server里事務默認是提交讀(Read Committed) 。
  鎖是對目標資源(行、頁、區、表..)獲取所有權的鎖定,是一個邏輯概念,用來保存事務的ACID. 當多用戶並發同時操作數據時,為了避免出現不一致的數據,鎖定是必須的機制。 但同時如果鎖的數量太多,持續時間太長,對系統的並發和性能都沒有好處。

三.鎖的全面認識

  3.1 鎖住的資源

  我們知道sql server的存儲數據單元包括文件組,頁,區,行。鎖住資源范圍從低到高依次對應的是:行(RID/KEY)鎖,頁(PAGE)鎖, 表(OBJECT)鎖。可通過sp_lock查看,比如: 當我們操作一條數據時應該是行鎖, 大批量操作時是頁鎖或表鎖, 這是大批量操作會使鎖的數量越多,鎖就會自動升級 將大量行鎖合成多個頁鎖或表鎖,來避免資源耗盡。SQL SERVER要鎖定資源時,默認是從最底級開始鎖起(行) 。鎖住的常見資源如下:

名稱

資源

說明

數據行 RID 鎖住堆中(表沒有建聚集索引)的單個行。格式為File:Page:SlotID  如 1:8787:4
索引鍵 KEY 鎖住T-tree(索引)中單個行,是一個哈值值。如:(fb00a499286b)                 
PAGE 鎖住數據頁(一頁8kb,除了頁頭和頁尾,頁內容存儲數據)可在sys.dm_os_buffer_descriptors找到。格式FileID :Page Number 如1:187541
范圍 extent 鎖住區(一組連續的8個頁 64kb)FileID:N頁 。如:1:78427
數據表 object 通常是鎖整個表。 如:2858747171
文件 File 一般是數據庫文件增加或移除時。如:1
數據庫 database 鎖住整個數據庫,比如設置修改庫為只讀模式時。 database ID如:7

    下圖是通過sp_lock的查看的,顯示了鎖住的資源類型以及資源

  3.2 鎖的類型及鎖說明

鎖類型 鎖說明
共享鎖 (S鎖) 用於不更改或不更新數據的讀取操作,如 SELECT 語句。
更新鎖 (U鎖) 它是S與X鎖的混合,更新實際操作是先查出所需的數據,為了保護這數據不會被其它事務修改,加上U鎖,在真正開始更新時,轉成X鎖。U鎖和S鎖兼容, 但X鎖和U鎖不兼容。
獨占鎖(排它鎖)(X鎖) 用於數據修改操作,例如 INSERT、UPDATE 或 DELETE。 確保不會同時對同一資源進行多重更新
意向鎖(I鎖) (I)鎖也不是單獨的鎖模式,用於建立鎖的層次結構。 意向鎖包含三種類型:意向共享 (IS)、意向排他 (IX) 和意向排他共享 (SIX)。意識鎖是用來標識一個資源是否已經被鎖定,比如一個事務嘗試鎖住一個表,首先會檢查是否已有鎖在該表的行或者頁上。
架構鎖(Sch-M,Sch-S) 在執行依賴於表架構操作時使用,例如:添加列或刪除列 這個時候使用的架構修改鎖(Sch-M),用來防止其它用戶對這個表格進行操作。別一種是數據庫引擎在編譯和執行查詢時使用架構性  (Sch-S),它不會阻止其它事務訪問表格里的數據,但會阻止對表格做修改性的ddl操作和dml操作。
大容量更新 (BU) 是指數據大容量復制到表中時使用BU鎖,它允許多個線程將數據並發地大容量加載到同一表,同時防止其它不進行大容量加載數據的進程訪問該表。
鍵范圍 當使用可序列化事務隔離級別時(SERIALIZABLE)保護查詢讀取的行的范圍。 確保再次運行查詢時其他事務無法插入符合可序列化事務的查詢的行。下章介紹的事務時再詳細說

四 鎖的互斥(兼容性)

  在sql server里有個表,來維護鎖與鎖之間的兼容性,這是sqlserver預先定義好的,沒有任務參數或配置能夠去修改它們。如何提高兼容性呢?那就是在設計數據庫結構和處理sql語句時應該考慮,盡量保持鎖粒度小,這樣產生阻塞的概率就會比較小,如果一個連接經常申請頁面級,表級,甚至是數據庫級的鎖資源,程序產生的阻塞的可能性就越大。假設:事務1要申請鎖時,該資源已被事務2鎖住,並且事務1要申請的鎖與事務2的鎖不兼容。事務1申請鎖就會出現wait狀態,直到事務2的鎖釋放才能申請到。 可通過sp_lock查看wait等待(也就是常說的阻塞) 

  下面是最常見的鎖模式的兼容性

五. 鎖與事務關系

  如今系統並發現象,引起的資源急用,出現的阻塞死鎖一直是技術人員比較關心的。這就涉及到了事務, 事務分五種隔離級別,每個隔離級別有一個特定的並發模式,不同的隔離級別中,事務里鎖的作用域,鎖持續的時間都不同,后面再詳細介紹事務。這里看下客戶端並發下的鎖與事務的關系, 可以理解事務是對鎖的封裝,事務就是在並發與鎖之間的中間層。如下圖:

  

六. 鎖的持續時間

  下面是鎖在不同事務隔離級別里,所持續占用的時間:

  6.1  SELECT動作要申請的鎖

    我們知道select 會申請到共享鎖,下面來演示下共享鎖在Repeatable 重復讀的級別下,共享鎖保留到事件提交時才釋放。

    具體是1.事務A設置隔離級別為Repeatable重復讀,開啟事務運行且不提交事務。

       2.再打開一個會話窗口,使用sys.dm_tran_locks來分析查看事務的持有鎖。 

--開啟一個事務A, 設置可重復讀, 不提交
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ 
BEGIN TRAN 
SELECT  * FROM dbo.Product WHERE SID=204144 
--上面執行完后,打開另一會話查詢鎖狀態
SELECT  k.request_session_id,k.resource_type,k.request_status,k.request_mode,k.resource_description,
 OBJECT_NAME( p.object_id) as objectName,p.index_id FROM SYS.dm_tran_locks k LEFT JOIN SYS.PARTITIONS p
ON k.resource_associated_entity_id=p.hobt_id
ORDER BY request_session_id,resource_type

    先看看查詢單條語句的執行計划,再看看鎖住的資源

    

    

   通過DMV查詢,我們看到:

    (1)首先是鎖住DATABASE資源,是數據庫級別的共享鎖,以防止別人將數據庫刪除。

    (2)鎖住OBJECT表資源,在Product表上加了意向共享鎖IS,以防止別人修改表的定義。

    (3)鎖住了二個PAGE頁加了意向共享鎖IS,通過上面執行計划可以看出來,查詢出來的數據是通過索引查詢50%,RID堆查詢50%。這條數據分布在二個頁上,通過where SID來查找沒有完全走索引查找。

    (4)通過第3點可以看出,數據1個頁是對應RID行,另一頁對應KEY行 二個共享鎖,堆位置1:112205:25  ,KEY的哈希值(70009fe3578a) 。

  總結下:通過Repeatable 重復讀,直要事務不提交,共享鎖一直會存在。針對想減少被別人阻塞或者阻塞別人的概率,能考慮事情有:1. 盡量減少返回的記錄,返回的記錄越多,需要的鎖也就越多,在Repeatable隔離級別及以上,更是容易造成阻塞。2.返回的數據如果是一小部份,盡量使用索引查找,避免全表掃描。3.可以的話,根據業務設計好最合適的幾個索引,避免通過多個索引找到結果。                                                  

  4.2  UPDATE動作要申請的鎖

    對於UPDATE需要先查詢,再修改。具體是查詢加S鎖,找到將要修改的記錄后先加U鎖,真正修改時升級成X鎖。還是通過上面的product表來演示具體:選用Repeatable級別,運行一個update語句(先kill 掉之前的會放52) 

--開啟一個事務, 設置可重復讀, 不提交
BEGIN TRAN 
UPDATE    dbo.Product SET model='test'
 WHERE SID IN(10905,119921,204144)

   

  通過 dmv查看,嚇一跳沒想到鎖住了這么多資源,糾結 那下面試着來分析下為什么鎖住這么多資源:使用sys.indexes查看index_id 的0,2,4各使用了什么索引

  SELECT  * FROM sys.indexes WHERE object_id= OBJECT_id('product')

  

  (1)這個product表並沒有建聚集索引,是在堆結構上建立的非索聚索引,index_id=0 是堆, index_id=2和4 又是分別二個非索聚索引

  (2)同樣在DATABASE和OBJECT資源 上都加了共享鎖。

  (3)意向排它鎖IX,鎖住的Page共9頁 說明數據關聯了9頁,其中堆上3頁,ix_1非索聚索引上3頁,ixUpByMemberID非索聚索引上3頁。 

  (4) 排它鎖X鎖住RID堆上3行,KEY索引上6行。大家可能會覺得奇怪明明只改三行的model值,為什么會涉及到9行呢?  我來解釋下這個表是建了三個非聚集索引,其中ix_1索引里有包含列model,xUpByMemberID索引里也同樣有包含列model,還有model數據是在堆,當堆上數據修改后,model關聯的非聚集索引也要重新維護。如下圖

   

  (5) 這里還有架構鎖Sch-s ,鎖住了元數據。

  總結:1.一定要給表做聚集索引,除了特殊情況使用堆結構。2.要修改的數據列越多,鎖的數目就會越多,這里model就涉及到了9行維護。3. 描述的頁面越多,意向鎖就會越多,對掃描的記錄也會加鎖,哪怕沒有修改。所以想減少阻塞要做到:1).盡量修改少的數據集,修改量越多,需要的鎖也就越多。2) 盡量減少無謂的索引,索引的數目越多,需要的鎖也可能越多。3.嚴格避免全局掃描,修改表格記錄時,盡量使用索引查詢來修改。

  4.3  DELETE動作要申請的鎖  

BEGIN TRAN 
DELETE     dbo.Product WHERE SID =10905

    

   (1) 刪除了RID堆的數據,以及關聯的非聚集索引三個key的值分別是(2,5,4)

   (2) 在要刪除的4個page上加了意向排它鎖,同樣對應一個RID和三個KEY。

   (3)在OBJECT資源表上加了意向排它鎖。

   總結:在DELETE過程中是先找到符合條件的記錄,然后再刪除, 可以說是先SELECT后DELETE,如果有索引第一步查詢申請的鎖會比較 少。 對於DELETE不但刪除數據本身,還會刪除所有相關的索引鍵,一個表上的索引越多,鎖的數目就會越多,也容易阻塞。為了防步阻塞我們不能不建索引,也不能隨便就建索引,而是要根據業務建查詢絕對有利的索引。

  4.4  INSERT動作要申請的鎖 

BEGIN TRAN 
INSERT into    dbo.Product VALUES('modeltest','brandtest',GETDATE(),9708,'test')

   

    對於以上三種動作,INSERT相對簡單點,只需要對要插入數據本身加上X鎖,對應的頁加IX鎖,同步更新了關聯的索引三個key。

    這里新增跟刪除最終顯示的鎖一樣,但在鎖申請的過程中,新增不需要先查詢到數據s鎖,升級u鎖,再升級成X鎖。

七. 鎖的升級

  7.1 使用profiler窗口查看實時的鎖升級

  以單次批操作受影響的行數超過5000條時(鎖數量最大值5000),升級為表鎖。在sqlserver里可以選擇完全關掉鎖升級,雖然可以減少阻塞,但鎖內存會增加,降低性能還可能造成更多死鎖。

 鎖升級缺點:會給其它會話帶來阻塞和死鎖。鎖升級優點:減少鎖的內存開銷。

  檢測方法:在profiler中查看lock:escalation事件類。通過查看Type列,可查看鎖升級的范圍,升級成表鎖(object是表鎖)

  如下圖:

    

  如果減少批操作量,就沒有看到升級表鎖, 可自行通過 escalation事件查看,下圖就是減少了受影響的行數。

    

  總結:將批操作量受影響行數減少到5000以下,減少鎖的升級后,發生了更頻繁的死鎖,原因是多個page頁的爭用。后有人指出你先把並行度降下來(刪除500一下的數據可以不使用並行) 在語句中設置maxdop = 1 這樣應該不會死鎖了。具體原因還需具體分析。

  7.2 使用dmv查看鎖升級

sys.dm_db_index_operational_stats返回數據庫中的當前較低級別 I/O、 鎖定、 閂鎖,和將表或索引的每個分區的訪問方法活動。

index_lock_promotion_attempt_count:數據庫引擎嘗試升級鎖的累積次數。

index_lock_promotion_count:數據庫引擎升級鎖的累積次數。

SELECT  OBJECT_NAME(ddios.[object_id], ddios.database_id) AS [object_name] ,
        i.name AS index_name ,
        ddios.index_id ,
        ddios.partition_number ,
        ddios.index_lock_promotion_attempt_count ,
        ddios.index_lock_promotion_count ,
        ( ddios.index_lock_promotion_attempt_count
          / ddios.index_lock_promotion_count ) AS percent_success
FROM    sys.dm_db_index_operational_stats(DB_ID(), NULL, NULL, NULL) ddios
        INNER JOIN sys.indexes i ON ddios.object_id = i.object_id
                                    AND ddios.index_id = i.index_id
WHERE   ddios.index_lock_promotion_count > 0
ORDER BY index_lock_promotion_count DESC;

  7.3 使用dmv查看頁級鎖資源爭用

  page_lock_wait_count:數據庫引擎等待頁鎖的累積次數。

  page_lock_wait_in_ms:數據庫引擎等待頁鎖的總毫秒數。

  missing_index_identified:缺失索引的表。

SELECT  OBJECT_NAME(ddios.object_id, ddios.database_id) AS object_name ,
        i.name AS index_name ,
        ddios.index_id ,
        ddios.partition_number ,
        ddios.page_lock_wait_count ,
        ddios.page_lock_wait_in_ms ,
        CASE WHEN DDMID.database_id IS NULL THEN 'N'
             ELSE 'Y'
        END AS missing_index_identified
FROM    sys.dm_db_index_operational_stats(DB_ID(), NULL, NULL, NULL) ddios
        INNER JOIN sys.indexes i ON ddios.object_id = i.object_id
                                    AND ddios.index_id = i.index_id
        LEFT OUTER JOIN ( SELECT DISTINCT
                                    database_id ,
                                    object_id
                          FROM      sys.dm_db_missing_index_details
                        ) AS DDMID ON DDMID.database_id = ddios.database_id
                                      AND DDMID.object_id = ddios.object_id
WHERE   ddios.page_lock_wait_in_ms > 0
ORDER BY ddios.page_lock_wait_count DESC;

八. 鎖的超時

   在sql server 里鎖默認是不會超時的,是無限的等待。多數客戶端編程允許用戶連接設置一個超時限制,因此在指定時間內沒有反饋,客戶端就會自動撤銷查詢, 但數據庫里鎖是沒有釋放的。

  可以通 select @@lock_timeout  查看默認值是 " -1", 可以修改超時時間  例如5秒超時 set  lock_timeout  5000;

     下面是查看鎖的等待時間, wait_time是當前會話的等待資源的持續時間(毫秒)

select  session_id, blocking_session_id,command,sql_handle,database_id,wait_type
,wait_time,wait_resource
from sys.dm_exec_requests 
where blocking_session_id>50


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM