備注
我們知道事務的重要性,我們同樣知道系統會出現並發,而且,一直在准求高並發,但是多數新手(包括我自己)經常忽略並發問題(更新丟失、臟讀、不可重復讀、幻讀),如何應對並發問題呢?和線程並發控制一樣,我們采用鎖(樂觀鎖和悲觀鎖),大多數場景我們不需要直接管理鎖,而是使用有更高語義的事務隔離級別來控制並發問題。
關於事務、事務隔離級別如何應對並發問題的文章我之前有過介紹,可以參考如下文章:.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 }
輸出
說明
修改鎖會阻塞其它更新鎖的獲取,因此所有任務都串行化了。
備注
最后補充一點,如果采用可串行化隔離級別,共享鎖不只會延長鎖定時間,鎖對應的資源的粒度也會變大(鎖表)。
從此不再迷茫,感覺入門了。