.NET:“事務、並發、並發問題、事務隔離級別、鎖”小議,重點介紹:“事務隔離級別"如何影響 “鎖”?


備注

我們知道事務的重要性,我們同樣知道系統會出現並發,而且,一直在准求高並發,但是多數新手(包括我自己)經常忽略並發問題(更新丟失、臟讀、不可重復讀、幻讀),如何應對並發問題呢?和線程並發控制一樣,我們采用鎖(樂觀鎖和悲觀鎖),大多數場景我們不需要直接管理鎖,而是使用有更高語義的事務隔離級別來控制並發問題。

關於事務、事務隔離級別如何應對並發問題的文章我之前有過介紹,可以參考如下文章:.NET:臟讀、不可重復讀和幻讀測試

本文重點說一下:事務隔離級別如何影響鎖?

事務隔離級別如何影響鎖?

這里大家只需知道兩種鎖:共享鎖和排它鎖,如果拿多線程相關的鎖來類比的話,共享鎖和排它鎖共同構成了:ReadWriteLockSlim,共享鎖是讀取鎖,排它鎖是修改鎖。一個資源只能擁有一個排他鎖,但是可以擁有多個共享鎖,而且共享鎖和排它鎖是互斥的,即:一個資源同時只能擁有一種鎖。

事務隔離級別不會影響排它鎖,修改資源的 SQL 會給資源加排它鎖,直到事務提交排它鎖才會釋放,如果此時有其它事務嘗試讀取修改的資源,讀取會被掛起,因為排它鎖是互斥的(使用了讀未提交隔離級別的事務除外)。事務隔離級別會影響共享鎖,如:是否需要共享鎖?擁有共享鎖多長時間?鎖定多大粒度的資源?下面我們看幾個具體的例子。

更新丟失(最后一個覆蓋前面的修改)

代碼

 1         private static void Test8()
 2         {
 3             using (var con = new SqlConnection(CONNECTION_STRING))
 4             {
 5                 con.Open();
 6                 var cmd = new SqlCommand("delete from Users", con);
 7                 cmd.ExecuteNonQuery();
 8 
 9                 cmd = new SqlCommand("insert into Users values (N'段光偉1')", con);
10                 cmd.ExecuteNonQuery();
11             }
12 
13             for (var i = 0; i < 5; i++)
14             {
15                 Task.Factory.StartNew((state) =>
16                 {
17                     using (var ts = new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted }))
18                     {
19                         using (var con = new SqlConnection(CONNECTION_STRING))
20                         {
21                             con.Open();
22 
23                             var index = (int)state + 1;
24                             var padding = new String(' ', (index - 1) * 3);
25 
26                             Console.WriteLine(string.Format("{0}第{1}次:開始讀取", padding, index));
27                             var cmd = new SqlCommand("select top 1 * from Users", con);
28                             cmd.ExecuteReader().Close();
29 
30                             Console.WriteLine(string.Format("{0}第{1}次:開始休眠", padding, index));
31                             System.Threading.Thread.Sleep(1000 * ((int)state + 1));
32 
33                             Console.WriteLine(string.Format("{0}第{1}次:開始修改", padding, index));
34                             cmd = new SqlCommand("update Users set Name = N'段光宇" + state + "'", con);
35                             cmd.ExecuteNonQuery();
36                             Console.WriteLine(string.Format("{0}第{1}次:完成修改", padding, index));
37                         }
38 
39                         ts.Complete();
40                     }
41                 }, i).ContinueWith((t) =>
42                 {
43                     Console.WriteLine(t.Exception.InnerException.Message);
44                 }, TaskContinuationOptions.OnlyOnFaulted);
45             }
46 
47             Console.ReadLine();
48         }

輸出

說明

此時會出現這種問題是因為:事務中的讀取雖然會使用共享鎖,但是共享鎖在讀取完成之后立即釋放,不會等到事務提交后才釋放,我們可以使用 SQL(注意看下面的 select 語句的變化) 延長共享鎖的持有時間,如下:

代碼

 1         private static void Test9()
 2         {
 3             using (var con = new SqlConnection(CONNECTION_STRING))
 4             {
 5                 con.Open();
 6                 var cmd = new SqlCommand("delete from Users", con);
 7                 cmd.ExecuteNonQuery();
 8 
 9                 cmd = new SqlCommand("insert into Users values (N'段光偉1')", con);
10                 cmd.ExecuteNonQuery();
11             }
12 
13             for (var i = 0; i < 5; i++)
14             {
15                 Task.Factory.StartNew((state) =>
16                 {
17                     using (var ts = new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted }))
18                     {
19                         using (var con = new SqlConnection(CONNECTION_STRING))
20                         {
21                             con.Open();
22 
23                             var index = (int)state + 1;
24                             var padding = new String(' ', (index - 1) * 3);
25 
26                             Console.WriteLine(string.Format("{0}第{1}次:開始讀取", padding, index));
27                             var cmd = new SqlCommand("select top 1 * from Users(holdlock)", con);
28                             cmd.ExecuteReader().Close();
29 
30                             Console.WriteLine(string.Format("{0}第{1}次:開始休眠", padding, index));
31                             System.Threading.Thread.Sleep(1000 * ((int)state + 1));
32 
33                             Console.WriteLine(string.Format("{0}第{1}次:開始修改", padding, index));
34                             cmd = new SqlCommand("update Users set Name = N'段光宇" + state + "'", con);
35                             cmd.ExecuteNonQuery();
36                             Console.WriteLine(string.Format("{0}第{1}次:完成修改", padding, index));
37                         }
38 
39                         ts.Complete();
40                     }
41                 }, i).ContinueWith((t) =>
42                 {
43                     Console.WriteLine(t.Exception.InnerException.Message);
44                 }, TaskContinuationOptions.OnlyOnFaulted);
45             }
46 
47             Console.ReadLine();
48         }

輸出

說明

我們為 select 語句采用了 (holdlock) 后綴,這回導致共享鎖直到事務提交才會釋放,好無疑問,這回導致死鎖,系統會選擇一個勝利者,其它的都作為犧牲品。

使用可重復讀隔離級別延長共享鎖的持有時間

上面我們手工采用 SQL 來延長了共享鎖的持有時間,這里演示另外一種方式。

代碼

 1         private static void Test10()
 2         {
 3             using (var con = new SqlConnection(CONNECTION_STRING))
 4             {
 5                 con.Open();
 6                 var cmd = new SqlCommand("delete from Users", con);
 7                 cmd.ExecuteNonQuery();
 8 
 9                 cmd = new SqlCommand("insert into Users values (N'段光偉1')", con);
10                 cmd.ExecuteNonQuery();
11             }
12 
13             for (var i = 0; i < 5; i++)
14             {
15                 Task.Factory.StartNew((state) =>
16                 {
17                     using (var ts = new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.RepeatableRead }))
18                     {
19                         using (var con = new SqlConnection(CONNECTION_STRING))
20                         {
21                             con.Open();
22 
23                             var index = (int)state + 1;
24                             var padding = new String(' ', (index - 1) * 3);
25 
26                             Console.WriteLine(string.Format("{0}第{1}次:開始讀取", padding, index));
27                             var cmd = new SqlCommand("select top 1 * from Users", con);
28                             cmd.ExecuteReader().Close();
29 
30                             Console.WriteLine(string.Format("{0}第{1}次:開始休眠", padding, index));
31                             System.Threading.Thread.Sleep(1000 * ((int)state + 1));
32 
33                             Console.WriteLine(string.Format("{0}第{1}次:開始修改", padding, index));
34                             cmd = new SqlCommand("update Users set Name = N'段光宇" + state + "'", con);
35                             cmd.ExecuteNonQuery();
36                             Console.WriteLine(string.Format("{0}第{1}次:完成修改", padding, index));
37                         }
38 
39                         ts.Complete();
40                     }
41                 }, i).ContinueWith((t) =>
42                 {
43                     Console.WriteLine(t.Exception.InnerException.Message);
44                 }, TaskContinuationOptions.OnlyOnFaulted);
45             }
46 
47             Console.ReadLine();
48         }

輸出

說明

使用事務隔離級別來控制共享鎖的持有時間,會影響整個事務內的所有讀取。

一種避免死鎖的方式

上面大家看到了死鎖的發生,我們可以采用樂觀鎖 + 重試來避免這種情況,當然也可以采用另外一種數據庫鎖:更新鎖(注意 select 語句的變化)。

代碼

 1         private static void Test11()
 2         {
 3             using (var con = new SqlConnection(CONNECTION_STRING))
 4             {
 5                 con.Open();
 6                 var cmd = new SqlCommand("delete from Users", con);
 7                 cmd.ExecuteNonQuery();
 8 
 9                 cmd = new SqlCommand("insert into Users values (N'段光偉1')", con);
10                 cmd.ExecuteNonQuery();
11             }
12 
13             for (var i = 0; i < 5; i++)
14             {
15                 Task.Factory.StartNew((state) =>
16                 {
17                     using (var ts = new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted }))
18                     {
19                         using (var con = new SqlConnection(CONNECTION_STRING))
20                         {
21                             con.Open();
22 
23                             var index = (int)state + 1;
24                             var padding = new String(' ', (index - 1) * 3);
25 
26                             Console.WriteLine(string.Format("{0}第{1}次:開始讀取", padding, index));
27                             var cmd = new SqlCommand("select top 1 * from Users(updlock)", con);
28                             cmd.ExecuteReader().Close();
29 
30                             Console.WriteLine(string.Format("{0}第{1}次:開始休眠", padding, index));
31                             System.Threading.Thread.Sleep(1000 * ((int)state + 1));
32 
33                             Console.WriteLine(string.Format("{0}第{1}次:開始修改", padding, index));
34                             cmd = new SqlCommand("update Users set Name = N'段光宇" + state + "'", con);
35                             cmd.ExecuteNonQuery();
36                             Console.WriteLine(string.Format("{0}第{1}次:完成修改", padding, index));
37                         }
38 
39                         ts.Complete();
40                     }
41                 }, i).ContinueWith((t) =>
42                 {
43                     Console.WriteLine(t.Exception.InnerException.Message);
44                 }, TaskContinuationOptions.OnlyOnFaulted);
45             }
46 
47             Console.ReadLine();
48         }

輸出

說明

修改鎖會阻塞其它更新鎖的獲取,因此所有任務都串行化了。

備注

最后補充一點,如果采用可串行化隔離級別,共享鎖不只會延長鎖定時間,鎖對應的資源的粒度也會變大(鎖表)。

從此不再迷茫,感覺入門了。

 


免責聲明!

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



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