淺析一個sql server數據庫事務死鎖問題

以前遇到過一個sql server數據庫事務死鎖問題,這裏記錄下來分享給你們。html

問題的原型java

爲了描述方便,這裏抽象問題的原型以下:sql

一個學生管理系統,數據庫是sql server,有一個Web API用於建立student。student對象的表結構以下:數據庫

CREATE TABLE [public].[A_Student](
  [id] [int] IDENTITY(1,1) NOT NULL,
  [name] [nvarchar](50) NOT NULL,
  [remark] [nvarchar](50) NULL
) ON [PRIMARY]

其中id是primary key。(note: primary key會自動建立一個clustered index)c#

建立一個student的實現邏輯能夠簡化爲下面一個事務(包含一個插入語句和一個查詢語句):併發

BEGIN TRAN
INSERT INTO public.[A_Student] ([name] ,[gender] ,[remark]) VALUES ('john', 'male','good student!')
SELECT [id] from public.[A_Student] where name = 'john'
COMMIT TRAN

在高併發測試過程當中發現,這段邏輯會發生事務死鎖問題,異常信息以下:分佈式

"Transaction (Process ID 60) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction."ide

問題的緣由高併發

後來研究發現,當上面的建立邏輯有兩個並行事務(T1和T2)交叉執行時,死鎖問題就會發生。具體緣由以下:post

T1和T2同時執行完insert語句,都會對新增的行加X鎖;而後,當T1和T2都執行select語句時,都須要申請全部行的S鎖(note: 因爲name字段沒有加index,因此須要執行clustered index scan),這時T1就pending在T2的X鎖上,T2則pending在T1的X鎖上,死鎖就發生了。

針對這個問題,有兩個解決方案:

1.把name字段加一個index;

2.把select語句加上with nolock

對於方案1,加上index以後,select語句就不會再有一個clustered index scan,只會是index seek,意味着只會申請某條記錄的S鎖,因此就不會發生死鎖。

對於方案2,把select語句加上with nolock後,語句執行時直接就不加鎖,鎖循環依賴就不存在了,死鎖也就解決了。固然,不加鎖,必定程度會出現髒讀,可是在這個業務場景下,不影響。

延申

1、沒有添加任何索引的時候,查詢語句(select id from table where name = 'john')的執行計劃是table scan;

當給id加上clustered index以後,語句的執行計劃是clustered index scan;

當給name加上index以後,語句的執行計劃就是index seek了。

爲何select的字段是id,where的條件是字段name,這裏會走index seek呢?

通常來講,select的字段須要是執行計劃用到的index包含的字段,這樣纔會走index seek,以下面語句:

select name from table where name = 'john'

但這裏走index seek卻應用到了另一個概念」覆蓋查詢「,具體含義以下:

當索引包含查詢中的全部列時,性能能夠提高。 查詢優化器能夠找到索引內的全部列值;不會訪問表或彙集索引數據,這樣就減小了磁盤 I/O 操做。 使用具備包含列的索引來添加覆蓋列,而不是建立寬索引鍵。

若是表有彙集索引,則該彙集索引中定義的列將自動追加到表上每一個非彙集索引的末端。 這能夠生成覆蓋查詢,而不用在非彙集索引定義中指定彙集索引列。 例如,若是一個表在 C列上有彙集索引,則 B 和 A 列的非彙集索引將具備其本身的鍵值列 B、 A和 C。

https://docs.microsoft.com/zh-cn/sql/relational-databases/sql-server-index-design-guide?view=sql-server-ver15#Nonclustered

從上面介紹能夠看到,彙集索引會自動加到每一個非彙集索引的後面造成覆蓋查詢,這就是爲何上面select id直接走index seek的緣由。

2、另外,在測試過程當中發現,當給name加上index以後,下面這條語句(select全部字段)的執行計劃是clustered index scan,而不是index seek + key lookup。

select * from table where name = 'John'

緣由是,在sql server中當表的數據量達到一個閾值(tipping point)的時候,執行計劃可能會發生變化。當時測試過程當中,表的數據量都很小,因此執行計劃是clustered index scan;後來,向表中插入1503條記錄以後,執行計劃就變成了make sense的index seek + key lookup。關於這個機制,能夠參考:

擴展

關於Index的實現原理,通常來講,index的實現都是基於B樹或者B+樹(在二叉查找樹BST的基礎上,減小磁盤IO);同時,不少數據庫都還支持一些其餘類型的index,好比哈希index,其實哈希index的底層原理就相似於java裏面的HashMap,c#裏面的Dictionary。

關於彙集索引和非彙集索引,其實有的數據庫並無實現這個概念,好比postgres。sql server實現了這兩個概念,詳細的介紹能夠參考(Clustered index: https://docs.microsoft.com/en-us/sql/relational-databases/sql-server-index-design-guide?view=sql-server-ver15#Clustered和non-clusterd index:https://docs.microsoft.com/en-us/sql/relational-databases/sql-server-index-design-guide?view=sql-server-ver15#Nonclustered

Microsoft sql server managment studio中查看執行計劃快捷鍵Ctrl+L;查看鎖使用狀況EXEC sp_lock。

相關閱讀

分佈式鎖在JPA ID生成器中的應用

liquibase和flyway中分佈式鎖實現的區別?

關於文本排序的那些事

相關文章
相關標籤/搜索