1. 程式人生 > >T-SQL查詢進階--SQL Server中的事務與鎖

T-SQL查詢進階--SQL Server中的事務與鎖

錯誤 span 設備 限制 數據復制 默認 base 數據 insert

為什麽需要鎖

在任何多用戶的數據庫中,必須有一套用於數據修改的一致的規則,當兩個不同的進程試圖同時修改同一份數據時,數據庫管理系統(DBMS)負責解決它們之間潛在的沖突。任何關系數據庫必須支持事務的ACID屬性,所以在開始了解鎖之前,首先簡單了解一下數據庫事務和事務的ACID屬性。

  • 原子性(Atomicity):原子性意味著數據庫中的事務執行是作為原子。即不可在分,整個語句要麽執行,要麽不執行
  • 一致性(Consistency):在事務開始之前和事務結束以後,數據庫的完整性約束沒有被破壞。(唯一約束,外鍵約束,Check約束等)和觸發器設置這一點有SQL SERVER進行保證的
  • 隔離線(Isolation):
    事務的執行是互不幹擾的,一個事務不可能看到其他事務運行時,中間某一時刻的數據
  • 持久性(Durability):持久性表示在某個事務的執行過程中,對數據所作的所以改的都必須在事務成功結束前保存某種物理存儲設備,這樣可以保證,所作的修改在任何系統癱瘓時不至於丟失。
  • 理論上所有的事務之間應該是完全隔離的。但是實際上,要實現完全隔離的成本實在是太高(必須是序列化的隔離等級才能完全隔離)。所以, SQL Server通過鎖,就像十字路口的紅綠燈那樣,告訴所有並發的連接,在同一時刻上,那些資源可以讀取,那些資源可以修改。當一個事務需要訪問的資源加了其所不兼容的鎖,SQL Server會阻塞當前的事務來達成所謂的隔離性。直到其所請求資源上的鎖被釋放。

    為此,SQL Server在隔離和並發之間選擇了Read Commited

    作為數據庫的默認隔離級別。

    多個用戶同時對數據庫的並發操作時會帶來以下數據不一致的問題:

    臟讀:一個事務讀取到了另外一個事務沒有提交的數據。

    A修改了數據,隨後B又讀出該數據,但A因為某些原因取消了對數據的修改,數據恢復原值,此時B得到的數據就與數據庫內的數據產生了不一致

    幻讀:同一事務中,用同樣的操作讀取兩次,得到的記錄數不相同。

    A讀取數據,隨後B又插入了數據,此時A再讀數據是發現前後兩次獲取的數據行集不一致

    不可重復讀:在同一事務中,兩次讀取同一數據,得到內容不同。

    A用戶讀取數據,隨後B用戶讀出該數據並修改,此時A用戶再讀取數據時發現前後兩次的值不一致

    丟失更新:事務T1讀取了數據,並執行了一些操作,然後更新數據。事務T2也做相同的事,則T1和T2更新數據時可能會覆蓋對方的更新,從而引起錯誤。

    A,B兩個用戶讀同一數據並進行修改,其中一個用戶的修改結果破壞了另一個修改的結果,比如訂票系統

    並發控制的主要方法是通過鎖,在一段時間內禁止用戶做某些操作以避免產生數據不一致

    理解SQL SERVER中的隔離級別

    為了避免上述幾種事務之間的影響,SQL Server通過設置不同的隔離等級來進行不同程度的避免。 SQL Server提供了5種選項來避免不同級別的事務之間的影響。隔離等級由低到高分別為:

    • 未提交讀(Read Uncommited):最高的性能,但可能出現臟讀,不可重復讀,幻讀
    • 已提交讀(Read commited):可能出現不可重復讀,幻讀
    • 可重復讀(Repeatable Read):可能出現幻讀
    • 序列化(Serializable):最低的性能,Range鎖會導致並發下降
    • 快照(SNOPSHOT):這個是通過在tempDB中創建一個額外的副本來避免臟讀,不可重復讀,會給tempDB造成額外負擔

    鎖的模式

    • 共享鎖(S鎖):用於讀取資源所加的鎖。擁有共享鎖的資源不能被修改。共享鎖默認情況下是讀取了資源馬上被釋放。
    • 排他鎖(X鎖): 和其它任何鎖都不兼容,包括其它排他鎖。排它鎖用於數據修改,當資源上加了排他鎖時,其他請求讀取或修改這個資源的事務都會被阻塞,知道排他鎖被釋放為止。
    • 更新鎖(U鎖): U鎖可以看作是S鎖和X鎖的結合,用於更新數據,更新數據時首先需要找到被更新的數據,此時可以理解為被查找的數據上了S鎖。當找到需要修改的數據時,需要對被修改的資源上X鎖。SQL Server通過U鎖來避免死鎖問題。因為S鎖和S鎖是兼容的,通過U鎖和S鎖兼容,來使得更新查找時並不影響數據查找,而U鎖和U鎖之間並不兼容,從而減少了死鎖可能性。
    • 意向鎖(IS IX IU):意向鎖與其說是鎖,倒不如說更像一個指示器。在SQL Server中,資源是有層次的,一個表中可以包含N個頁,而一個頁中可以包含N個行。當我們在某一個行中加了鎖時。可以理解成包含這個行的頁,和表的一部分已經被鎖定。當另一個查詢需要鎖定頁或是表時,再一行行去看這個頁和表中所包含的數據是否被鎖定就有點太痛苦了。因此SQL Server鎖定一個粒度比較低的資源時,會在其父資源上加上意向鎖,告訴其他查詢這個資源的某一部分已經上鎖。比如,當我們更新一個表中的某一行時,其所在的頁和表都會獲得意向排他鎖
    • 快照(SNOPSHOT):這個是通過在tempDB中創建一個額外的副本來避免臟讀,不可重復讀,會給tempDB造成額外負擔
    • 鍵範圍鎖(KEY-RANGE):在使用可序列化事務隔離級別時,對於 Transact-SQL 語句讀取的記錄集,鍵範圍鎖可以隱式保護該記錄集中包含的行範圍。可序列化隔離級別要求每當在事務期間執行任一查詢時,該查詢都必須獲取相同的行集。鍵範圍鎖可防止其他事務插入其鍵值位於可序列化事務讀取的鍵值範圍內的新行,從而確保滿足此要求。
    • 鍵範圍鎖可防止幻讀。通過保護行之間的鍵範圍,它還可以防止對事務訪問的記錄集進行幻插入。

      鍵範圍鎖放置在索引上,指定開始鍵值和結束鍵值。此鎖將阻止任何要插入、更新或刪除任何帶有該範圍內的鍵值的行的嘗試,因為這些操作會首 先獲取索引上的鎖。例如,可序列化事務可能發出了一個 SELECT 語句,以讀取其鍵值介於 ‘AAA‘ 與 ‘CZZ‘ 之間的所有行。從 ‘AAA‘ 到 ‘CZZ‘ 範圍內的鍵值上的鍵範圍鎖可阻止其他事務插入帶有該範圍內的鍵值(例如 ‘ADG‘、‘BBD‘ 或 ‘CAL‘)的行。

      • 架構鎖: SQL Server 使用架構鎖來保持表結構的完整性。不像其他提供數據隔離的鎖類型,架構鎖提供事務中對數據庫對象如表、視圖、索引的schema隔離。
      • 大容量更新鎖:在向表進行大容量數據復制且指定了 TABLOCK 提示時使用
      • 鎖兼容性

        技術分享圖片

    鎖的粒度

    所謂所粒度,從本質上說就是,為了給事務提供完全的隔離和序列化,作為查詢或更新的一部分被鎖定的數據的總量(的大小)。Lock Manager需要在資源的並發訪問與維護大量低級別鎖的管理開銷之間取得平衡。比如,鎖的粒度越小,能夠同時訪問同一張表的並發用戶的數量就越大,不過維護這些鎖的管理開銷也越大。鎖的粒度越大,管理鎖需要的開銷就越少,而並發性也降低了。下圖說明了鎖的大小與並發性之間的權衡取舍。

    技術分享圖片

    SQL Server支持的鎖粒度可以分為為行、頁、鍵、鍵範圍、索引、表或數據庫獲取鎖

    鎖升級

    鎖升級是將許多較細粒度的鎖轉換成數量更少的較粗粒度的鎖的過程,這樣可以減少系統開銷,但卻增加了並發爭用的可能性。

    當 SQL Server 數據庫引擎獲取低級別的鎖時,它還將在包含更低級別對象的對象上放置意向鎖:

    1. 當鎖定行或索引鍵範圍時,數據庫引擎將在包含這些行或鍵的頁上放置意向鎖。
    2. 當鎖定頁時,數據庫引擎將在包含這些頁的更高級別的對象上放置意向鎖。

    除了對象上的意向鎖以外,以下對象上還需要意向頁鎖:非聚集索引的葉級頁、聚集索引的數據頁、堆數據頁。

    鎖升級的閾值:

    • 單個 Transact-SQL 語句在單個無分區表或索引上獲得至少 5,000 個鎖。
    • 單個 Transact-SQL 語句在已分區表的單個分區上獲得至少 5,000 個鎖,並且 ALTER TABLE SET LOCK_ESCALATION 選項設為 AUTO。
    • 數據庫引擎實例中的鎖的數量超出了內存或配置閾值

    TIPS:數據庫引擎不會將行鎖或鍵範圍鎖升級到頁鎖,而是將它們直接升級到表鎖。同樣,頁鎖始終升級到表鎖。

    --查看鎖活動情況
    select * from sys.dm_tran_locks
    --查看事務活動情況
    dbcc opentran
     
    可參考 :https://docs.microsoft.com/zh-cn/sql/relational-databases/system-dynamic-management-views/sys-dm-tran-locks-transact-sql?view=sql-server-2017

    什麽是死鎖

    什麽是死鎖

    死鎖的本質是一種僵持狀態,是多個主體對於資源的爭用而導致的。在兩個或多個任務中,如果每個任務鎖定了其他任務試圖鎖定的資源,此時會造成這些任務永久阻塞,從而出現死鎖。理解死鎖首先需要對死鎖所涉及的相關觀念有一個理解。

    技術分享圖片

    在上圖的例子中,每隊汽車都占有一條道路,但都需要另外一隊汽車所占有的另一條道路,因此互相阻塞,誰都無法前行,因此造成了死鎖。


    死鎖產生的原因及四個必要條件

    產生死鎖的原因主要是:

    (1) 因為系統資源不足。

    (2) 進程運行推進的順序不合適。

    (3) 資源分配不當等。

    如果系統資源充足,進程的資源請求都能夠得到滿足,死鎖出現的可能性就很低,否則就會因爭奪有限的資源而陷入死鎖。其次,進程運行推進順序與速度不同,也可能產生死鎖。

    產生死鎖的四個必要條件:

    (1) 互斥條件:一個資源每次只能被一個進程使用。

    (2) 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。

    (3) 不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。

    (4) 循環等待條件:若幹進程之間形成一種頭尾相接的循環等待資源關系。

    這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不滿足,就不會發生死鎖。

    死鎖的兩種類型

    1、循環死鎖:兩個進程請求不同資源上的鎖,每一個進程都需要對方持有的該資源上的鎖,這時將發生循環死鎖。如下圖

    技術分享圖片

    2、轉換死鎖:兩個或多個進程都在事務中持有同一資源上的共享鎖,並且都想把它升級為獨占鎖,但是,誰也沒法升級直到其他的進程釋放共享鎖,如下圖

    技術分享圖片

    SQL Server中產生死鎖的一些情況

    1、由書簽查找產生的死鎖:這類死鎖產生的原因是書簽查找和更新數據產生的僵持狀態。簡單來說,就是由於Update語句對基本表產生X鎖,然後需要對表上的索引也進行更新,而表上的索引正好被另一個連接進行查找,加了S鎖,此時又產生書簽查找去基本表加了X鎖的數據進行書簽查找,此時形成死鎖

    技術分享圖片

    書簽查找:當查詢優化器使用非聚集索引進行查找時,如果所選擇的列或查詢條件中的列只部分包含在使用的非聚集索引和聚集索引中時,就需要一個查找(lookup)來檢索其他字段來滿足請求。對一個有聚簇索引的表來說是一個鍵查找(key lookup),對一個堆表來說是一個RID查找(RID lookup),這種查找即是——書簽查找(bookmark lookup)。簡單的說就是當你使用的sql查詢條件和select返回的列沒有完全包含在索引列中時就會發生書簽查找。

    解決方案:這種死鎖可以通過Include列來減少書簽查找,從而減少這種類型死鎖發生的概率。

    2、由外鍵產生的死鎖: 這類死鎖產生的原因來自外鍵約束。當主表(也就是主鍵是從表外鍵的那個表)更新數據時,需要查看從表,以確定從表的外鍵列滿足外鍵約束。此時會在主表上加X鎖,但這並不能阻止同一時間,另一個SPID向從表添加被修改的主表主鍵,為了解決這個問題,SQL Server在進行這類更新時,使用Range鎖,這種鎖是當隔離等級為序列化時才有的,因此在這時雖然隔離等級可能是默認的已提交讀,但是行為卻是序列化。這很可能就會導致死鎖。

    解決方案:向外鍵列添加索引,使得Range鎖加在索引上,而不是表本身。從而降低了死鎖發生的概率。

    3、由於推進順序不當產生的死鎖:在多個事務對資源的使用順序不當,形成死鎖環路而引發的。

    技術分享圖片

    解決方案:盡量使資源的使用順序一致。這也是死鎖問題出現最多的一種情況。

    如何查看死鎖

    技術分享圖片

    技術分享圖片

    三、死鎖的預防與優化

    預防死鎖

    預防死鎖就是破壞四個必要條件中的某一個和幾個,使其不能形成死鎖。有如下幾種辦法:

    1)破壞互斥條件

    破壞互斥條件有比較嚴格的限制,在SQL Server中,如果業務邏輯上允許臟讀,則可以通過將隔離等級改為未提交讀或使用索引提示。這樣使得讀取不用加S鎖,從而避免了和其它查詢所加的與S鎖不兼容的鎖互斥,進而減少了死鎖出現的概率。

    2)破壞請求和等待條件

    這點由於事務存在原子性,是不可破壞的,因為解決辦法是盡量的減少事務的長度,事務內執行的越快越好。這也可以減少死鎖出現的概率。

    3)破壞不剝奪條件

    由於事務的原子性和一致性,不剝奪條件同樣不可破壞。但我們可以通過增加資源和減少資源占用兩個角度來考慮。

    增加資源:比如說通過建立非聚集索引,使得有了額外的資源,查詢很多時候就不再索要鎖基本表,轉而鎖非聚集索引,如果索引能夠"覆蓋(Cover)"查詢,那更好不過。因此索引Include列不僅僅減少書簽查找來提高性能,還能減少死鎖。

    減少資源占用:比如說查詢時,能用select col1,col2這種方式,就不要用select * .這有可能帶來不必要的書簽查找

    最大限度減少死鎖的方法

    1. 按同一順序訪問對象: 按同一順序訪問對象也就是:第一個事務提交或回滾後,第二個事務繼續進行,這樣不會發生死鎖。
    2. 避免事務中的用戶交互: 避免編寫包含用戶交互的事務,因為運行沒有用戶交互的批處理的速度要遠遠快於用戶手動響應查詢的速度,例如答復應用程序請求參數的提示。例如,如果事務正在等待用戶輸入,而用戶去吃午餐了或者甚至回家過周末了,則用戶將此事務掛起使之不能完成。這樣將降低系統的吞吐量,因為事務持有的任何鎖只有在事務提交或回滾時才會釋放。即使不出現死鎖的情況,訪問同一資源的其它事務也會被阻塞,等待該事務完成。
    3. 保持事務簡短並在一個批處理中: 在同一數據庫中並發執行多個需要長時間運行的事務時通常發生死鎖。事務運行時間越長,其持有排它鎖或更新鎖的時間也就越長,從而堵塞了其它活動並可能導致死鎖。 保持事務在一個批處理中,可以最小化事務的網絡通信往返量,減少完成事務可能的延遲並釋放鎖。
    4. 使用低隔離級別: 確定事務是否能在更低的隔離級別上運行,執行提交讀取允許事務讀取另一個事務已讀取(未修改)的數據,而不必等待第一個事務完成。使用較低的隔離級別(例如提交讀取)而不使用較高的隔離級別(例如可串行讀)可以縮短持有共享鎖的時間,從而降低了鎖定爭奪。
    5. 使用綁定連接: 使用綁定連接使同一應用程序所打開的兩個或多個連接可以相互合作。次級連接所獲得的任何鎖可以象由主連接獲得的鎖那樣持有,反之亦然,因此不會相互阻塞。

    優化死鎖的一些建議

    (1)對於查詢頻繁的表盡量使用聚集索引;

    (2)設法避免一次性影響大量記錄的SQL語句,特別是INSERT和UPDATE語句;

    (3)設法讓UPDATE和DELETE語句使用合適的索引;

    (4)使用嵌套事務時,避免提交和回退沖突;

    (5)對數據一致性要求不高的查詢使用 WITH(NOLOCK)

    (6)減小事務的體積,事務應最晚開啟,最早關閉,所有不是必須使用事務的操作必須放在事務外。

    (7)查詢只返回你需要的列,不建議使用 SELECT * FROM 這種寫法。

可參考:https://www.cnblogs.com/knowledgesea/p/3714417.html

T-SQL查詢進階--SQL Server中的事務與鎖