最近在分析SQL Server的死鎖時,發現一個比較有意思的現象,發現死鎖當中一個會話的隔離級別爲序列化(Serializable),這個是讓人比較奇怪的地方,咱們知道SQL Server數據庫的默認隔離級別爲已提交讀(READ COMMITTED),除非人爲設置事務隔離級別(TRANSACTION ISOLATION LEVEL),不然事務隔離級別會使用數據庫的默認隔離級別。在分析了死鎖相關的存儲過程後,沒有發現有人爲修改事務隔離級別的地方。在分析事後,咱們判斷應該是在應用程序代碼裏面有設置隔離級別,下面咱們經過一個小實驗來構造這樣的一個案例。sql
測試環境數據庫爲AdventureWorks2014,以下所示,我簡單寫了一點C#代碼,截取黏貼部分C#代碼在此,在這段代碼中,咱們使用TransactionScope,咱們先更新Sales.SalesOrderDetail,而後查詢 [Sales].[SalesOrderHeader]的相關數據來綁定Grid控件數據庫
try
{
using (TransactionScope scope = new TransactionScope())
{
using (SqlConnection conn = new SqlConnection(connString))
{
string cmdText = "UPDATE Sales.SalesOrderDetail SET OrderQty=2 WHERE SalesOrderID=43659 AND SalesOrderDetailID=1;";
SqlCommand cmd = new SqlCommand(cmdText, conn);
conn.Open();
cmd.ExecuteNonQuery();
}
using (SqlConnection conn = new SqlConnection(connString))
{
DataSet sqldataset = new DataSet();
string cmdText = "SELECT * FROM [Sales].[SalesOrderHeader] WHERE SalesOrderID=43659;";
SqlCommand cmd = new SqlCommand(cmdText, conn);
SqlDataAdapter sqladapter = new SqlDataAdapter(cmdText, conn);
sqladapter.Fill(sqldataset, "spt_values");
gvData.DataSource = sqldataset;
gvData.DataBind();
}
scope.Complete();
}
}
catch (TransactionAbortedException exc)
{
log.Error("錯誤", exc);
}
而後另一個會話,就直接用SSMS開啓一個事務(懶得構造C#代碼案例,主要是太浪費時間了),主要執行下面邏輯:session
BEGIN TRAN
UPDATE [Sales].[SalesOrderHeader] SET SubTotal = SubTotal + 10
WHERE SalesOrderID=43659;
WAITFOR DELAY '00:00:10';
SELECT TOP 10 * FROM Sales.SalesOrderDetail
--ROLLBACK TRAN;
執行上面SQL語句,而後運行最上面C#代碼,立馬就能構造出一個死鎖案例,以下截圖所示,測試環境爲SQL Server 2014,我就使用擴展事件system_health捕獲的死鎖(固然,你能夠使用任何方式,例如Profile或Trace捕獲死鎖相關信息),使用SQL將死鎖的XML信息查出併發
以下所示,你會看到使用TransactionScope的會話的隔離級別爲isolationlevel="serializable (4)", 具體能夠參考下面死鎖的XML文件。app
<deadlock>
<victim-list>
<victimProcess id="process17676e108" />
</victim-list>
<process-list>
<process id="process17676e108" taskpriority="0" logused="384" waitresource="KEY: 7:72057594048479232 (0ca7b7436f59)" waittime="379" ownerId="46635671" transactionname="user_transaction" lasttranstarted="2019-04-02T23:26:21.150" XDES="0x17f0511f0" lockMode="S" schedulerid="1" kpid="13440" status="suspended" spid="61" sbid="0" ecid="0" priority="0" trancount="1" lastbatchstarted="2019-04-02T23:26:21.147" lastbatchcompleted="2019-04-02T23:26:09.343" lastattention="1900-01-01T00:00:00.343" clientapp="Microsoft SQL Server Management Studio - Query" hostname="MyNB00021" hostpid="9728" loginname="test" isolationlevel="read committed (2)" xactid="46635671" currentdb="7" lockTimeout="4294967295" clientoption1="671090784" clientoption2="390200">
<executionStack>
<frame procname="adhoc" line="8" stmtstart="282" stmtend="368" sqlhandle="0x020000002a285923f5e38f7347b53337195c56a4a1bc33080000000000000000000000000000000000000000">
unknown </frame>
</executionStack>
<inputbuf>
BEGIN TRAN
UPDATE [Sales].[SalesOrderHeader] SET SubTotal = SubTotal + 10
WHERE SalesOrderID=43659;
WAITFOR DELAY '00:00:10';
SELECT TOP 10 * FROM Sales.SalesOrderDetail </inputbuf>
</process>
<process id="process175603c28" taskpriority="0" logused="436" waitresource="KEY: 7:72057594048544768 (6a8a6db47ef5)" waittime="4420" ownerId="46635065" transactionname="user_transaction" lasttranstarted="2019-04-02T23:25:36.807" XDES="0x1762fa9f0" lockMode="S" schedulerid="1" kpid="51760" status="suspended" spid="63" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2019-04-02T23:26:26.450" lastbatchcompleted="2019-04-02T23:25:36.807" lastattention="1900-01-01T00:00:00.807" clientapp=".Net SqlClient Data Provider" hostname="MyNB00021" hostpid="1700" loginname="kkk" isolationlevel="serializable (4)" xactid="46635065" currentdb="7" lockTimeout="4294967295" clientoption1="673316896" clientoption2="128056">
<executionStack>
<frame procname="AdventureWorks2014.Sales.iduSalesOrderDetail" line="18" stmtstart="982" stmtend="2448" sqlhandle="0x0300070076146e6c18e00a016ba3000000000000000000000000000000000000000000000000000000000000">
INSERT INTO [Production].[TransactionHistory]
([ProductID]
,[ReferenceOrderID]
,[ReferenceOrderLineID]
,[TransactionType]
,[TransactionDate]
,[Quantity]
,[ActualCost])
SELECT
inserted.[ProductID]
,inserted.[SalesOrderID]
,inserted.[SalesOrderDetailID]
,'S'
,GETDATE()
,inserted.[OrderQty]
,inserted.[UnitPrice]
FROM inserted
INNER JOIN [Sales].[SalesOrderHeader]
ON inserted.[SalesOrderID] = [Sales].[SalesOrderHeader].[SalesOrderID </frame>
<frame procname="adhoc" line="1" stmtstart="52" stmtend="262" sqlhandle="0x02000000abf4ee0ff24fea415c6f35709c721203030a173b0000000000000000000000000000000000000000">
unknown </frame>
<frame procname="adhoc" line="1" stmtend="186" sqlhandle="0x02000000b0cd40243d43ed1a51b1baa9cbf70d1628eae7880000000000000000000000000000000000000000">
unknown </frame>
</executionStack>
<inputbuf>
UPDATE Sales.SalesOrderDetail SET OrderQty=2 WHERE SalesOrderID=43659 AND SalesOrderDetailID=1; </inputbuf>
</process>
</process-list>
<resource-list>
<keylock hobtid="72057594048479232" dbid="7" objectname="AdventureWorks2014.Sales.SalesOrderDetail" indexname="PK_SalesOrderDetail_SalesOrderID_SalesOrderDetailID_old" id="lock154ffb300" mode="X" associatedObjectId="72057594048479232">
<owner-list>
<owner id="process175603c28" mode="X" />
</owner-list>
<waiter-list>
<waiter id="process17676e108" mode="S" requestType="wait" />
</waiter-list>
</keylock>
<keylock hobtid="72057594048544768" dbid="7" objectname="AdventureWorks2014.Sales.SalesOrderHeader" indexname="PK_SalesOrderHeader_SalesOrderID" id="lock155a8fa00" mode="X" associatedObjectId="72057594048544768">
<owner-list>
<owner id="process17676e108" mode="X" />
</owner-list>
<waiter-list>
<waiter id="process175603c28" mode="S" requestType="wait" />
</waiter-list>
</keylock>
</resource-list>
</deadlock>
咱們也能夠使用下面SQL語句來捕獲會話的的隔離級別(根據實際狀況調整), 在運行上面C#代碼期間捕獲會話信息,以下截圖所示:ide
DECLARE @end_time DATETIME;
SET @end_time = DATEADD(SECOND, 10, GETDATE());
WHILE GETDATE() < @end_time
BEGIN
INSERT INTO mintor_isolcation_level
SELECT session_id ,
start_time ,
status ,
total_elapsed_time ,
CASE transaction_isolation_level
WHEN 1 THEN 'ReadUncomitted'
WHEN 2 THEN 'ReadCommitted'
WHEN 3 THEN 'Repeatable'
WHEN 4 THEN 'Serializable'
WHEN 5 THEN 'Snapshot'
ELSE 'Unspecified'
END AS transaction_isolation_level ,
sh.text ,
ph.query_plan
FROM sys.dm_exec_requests
CROSS APPLY sys.dm_exec_sql_text(sql_handle) sh
CROSS APPLY sys.dm_exec_query_plan(plan_handle) ph
END
由於上面的腳本執行時間過短,因此有可能捕獲到的是相關SQL運行期間的觸發器腳本。若是要清晰的捕獲相關SQL,能夠構造一個執行時間較長的SQL函數
是否有點意外,其實官方文檔已有詳細介紹(詳見參考資料),摘抄部分信息以下,TransactionScope若是不指定隔離級別,默認狀況下,事務隔離級別爲Serializable測試
設置 TransactionScope 隔離級別spa
除超時值以外,TransactionScope 的有些重載構造函數還接受 TransactionOptions 類型的結構,用於指定隔離級別。 默認狀況下,事務在隔離級別設置爲 Serializable 的狀況下執行。 一般對頻繁執行讀取的系統選擇 Serializable 以外的隔離級別。 這須要全面地瞭解事務處理理論、事務自己的語義、所涉及的併發問題以及系統一致性的結果。3d
總結
這裏只是一個案例,僅僅說明應用程序的驅動程序或API函數,有可能會須要(或默認)設定事務的隔離級別,這個必定要小心,避免因爲人爲失誤導(不瞭解技術細節)致不當心提升事務隔離級別,形成沒必要要的死鎖出現。另外,這裏總結這篇文章,也僅僅是對這種案例感到有意思。
參考資料:
https://docs.microsoft.com/en-us/dotnet/framework/data/transactions/implementing-an-implicit-transaction-using-transaction-scope