SqlServer鎖機制與實踐

 

在現在這個雲計算,大數據,移動互聯網大行其道的時代,各類NoSQL數據庫MongoDb、redis、HBase等使用的愈來愈普遍,大有替代關係型數據庫的趨勢。可是關係型數據庫真的已經落伍了嗎?答案是否認的。非關係型數據庫不支持ACID屬性,不支持事務,沒法適應複雜查詢的缺點。關係型數據庫憑藉其強一致性的特色,註定了在相似銀行轉帳,訂單支付等場景中,仍是惟一的選擇。衆所周知,SQLSERVER經過鎖來提供ACID屬性,處理併發訪問,本文嘗試經過對鎖機制的一些學習,讓咱們明白數據庫查詢超時,死鎖等問題是如何產生的,及相關解決方法。 redis

在開始談有關鎖的內容前,咱們先看幾個咱們在平常開發中常常碰到的場景: sql

  1. 這個查詢我索引該加的都加了,測試庫剛同步的生產庫,在測試環境查詢秒出,爲何到了生產環境就會常常超時呢?
  2. 個人某個業務邏輯上線後一切正常,運行了幾個月後,開始死鎖愈來愈頻繁了,可是數據量也不大,爲何呢?

以上兩種狀況或許是由於鎖在其中"做怪"。下面咱們就開始從一些鎖的基本概念談起,瞭解SQLSERVER的鎖機制,死鎖成因以及死鎖的預防。爲了讓你們能對鎖有一個更深刻的瞭解,本文前半章談的比較多的是一些基本概念。老鳥能夠自動略過。 數據庫

 

一.鎖的一些基本概念 網絡

爲何須要鎖?架構

在任何多用戶的數據庫中,必須有一套用於數據修改的一致的規則,當兩個不一樣的進程試圖同時修改同一份數據時,數據庫管理系統(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:數據庫引擎不會將行鎖或鍵範圍鎖升級到頁鎖,而是將它們直接升級到表鎖。一樣,頁鎖始終升級到表鎖。

 

如何查看鎖

一、使用sys.dm_tran_locks這個DMV

二、使用Profiler來捕捉鎖信息

 

2、死鎖成因分析

什麼是死鎖

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

在上圖的例子中,每隊汽車都佔有一條道路,但都須要另一隊汽車所佔有的另外一條道路,所以互相阻塞,誰都沒法前行,所以形成了死鎖。

 

死鎖產生的緣由及四個必要條件

產生死鎖的緣由主要是:

(1) 由於系統資源不足。

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

(3) 資源分配不當等。

若是系統資源充足,進程的資源請求都可以獲得知足,死鎖出現的可能性就很低,不然就會因爭奪有限的資源而陷入死鎖。其次,進程運行推動順序與速度不一樣,也可能產生死鎖。

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

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

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

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

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

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

 

死鎖的兩種類型

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

二、轉換死鎖:兩個或多個進程都在事務中持有同一資源上的共享鎖,而且都想把它升級爲獨佔鎖,可是,誰也無法升級直到其餘的進程釋放共享鎖,以下圖

 

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

一、由書籤查找產生的死鎖:這類死鎖產生的緣由是書籤查找和更新數據產生的僵持狀態。簡單來講,就是因爲Update語句對基本表產生X鎖,而後須要對錶上的索引也進行更新,而表上的索引正好被另外一個鏈接進行查找,加了S鎖,此時又產生書籤查找去基本表加了X鎖的數據進行書籤查找,此時造成死鎖 

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

解決方案:這種死鎖能夠經過Include列來減小書籤查找,從而減小這種類型死鎖發生的機率。

 

二、由外鍵產生的死鎖: 這類死鎖產生的緣由來自外鍵約束。當主表(也就是主鍵是從表外鍵的那個表)更新數據時,須要查看從表,以肯定從表的外鍵列知足外鍵約束。此時會在主表上加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 這種寫法。

相關文章
相關標籤/搜索