最近在園子裏看到一篇關於TransactionScope的文章,發現事務和併發控制是新接觸Entity Framework和Transaction Scope的園友們不易理解的問題,遂組織此文跟你們共同探討。數據庫
首先事務的ACID特性做爲最基礎的知識我想你們都應該知道了。ADO.NET的SQLTransaction就是.NET框架下訪問SqlServer時最底層的數據庫事務對象,它能夠用來將屢次的數據庫訪問封裝爲「原子操做」,也能夠經過修改隔離級別來控制併發時的行爲。TransactionScope則是爲了在分佈式數據節點上完成事務的工具,它常常被用在業務邏輯層將多個數據庫操做組織成業務事務的場景,能夠作到透明的可分佈式事務控制和隱式失敗回滾。但與此同時也常常有人提到TransactionScope有性能和部署方面的問題,關於這一點,根據MSDN的 Using the TransactionScope Class 的說法,當一個TransactionScope包含的操做是同一個數據庫鏈接時,它的行爲與SqlTransaction是相似的。當它在多個數據庫鏈接上進行數據操做時,則會將本地數據庫事務提高爲分佈式事務,而這種提高要求各個節點均安裝並啓動DTC服務來支持分佈式事務的協調工做,它的性能與本地數據庫事務相比會低不少,這也是CAP定律說的分佈式系統的Consistency和Availability不可兼得的典型例子。因此當咱們選擇是否使用TransactionScope時,必定要確認它會不會致使不想發生的分佈式事務,也應該確保事務儘快作完它該作的事情,爲了確認事務是否被提高咱們能夠用SQL Profiler去跟蹤相關的事件。併發
而後再來看一看Entity Framework,其實EF也跟事務有關係。它的Context概念來源於Unit of Work模式,Context記錄提交前的全部Entity變化,並在SaveChanges方法調用時發起真正的數據庫操做,SaveChanges方法在默認狀況下隱含一個事務,而且試圖使用樂觀併發控制來提交數據,可是爲了進行併發控制咱們須要將Entity Property的ConcurrencyMode設置爲Fixed才行,不然EF不理會在此Entity上面發生的併發修改,這一點能夠參考MSDN Saving Changes and Managing Concurrency。微軟推薦你們使用如下方法來捕獲衝突的併發操做,並使用RefreshMode來選擇覆蓋或丟棄失敗的操做:框架
1 try 2 { 3 // Try to save changes, which may cause a conflict. 4 int num = context.SaveChanges(); 5 Console.WriteLine("No conflicts. " + 6 num.ToString() + " updates saved."); 7 } 8 catch (OptimisticConcurrencyException) 9 { 10 // Resolve the concurrency conflict by refreshing the 11 // object context before re-saving changes. 12 context.Refresh(RefreshMode.ClientWins, orders); 13 14 // Save changes. 15 context.SaveChanges(); 16 Console.WriteLine("OptimisticConcurrencyException " 17 + "handled and changes saved"); 18 }
固然除了樂觀併發控制咱們還能夠對衝突特別頻繁、衝突解決代價很大的用例進行悲觀併發控制。悲觀併發基本思想是不讓數據被同時離線修改,也就是像源碼管理裏面「加鎖」功能同樣,碼農甲鎖上了這個文件,乙就不能再修改了,這樣一來這個文件就不可能發生衝突,悲觀併發控制實現的方式好比數據行加IsLocked字段等。分佈式
最後爲了進行多Context事務,固然還能夠混合使用TransactionScope和EF。ide
好了理論簡單介紹完,下面的例子是幾種不一樣的方法對併發控制的效果,需求是每一個Member都有個HasMessage字段,初始爲False,咱們須要給其中一個Member加入惟一一條MemberMessage,並將Member.HasMessage置爲True。工具
建庫腳本:性能
1 CREATE DATABASE [TransactionTest] 2 GO 3 4 USE [TransactionTest] 5 GO 6 7 CREATE TABLE [dbo].[Member]( 8 [Id] [int] IDENTITY(1,1) NOT NULL, 9 [Name] [nvarchar](32) NOT NULL, 10 [HasMessage] [bit] NOT NULL, 11 CONSTRAINT [PK_Member] PRIMARY KEY CLUSTERED 12 ( 13 [Id] ASC 14 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 15 ) ON [PRIMARY] 16 GO 17 SET IDENTITY_INSERT [dbo].[Member] ON 18 INSERT [dbo].[Member] ([Id], [Name], [HasMessage]) VALUES (1, N'Tom', 0) 19 INSERT [dbo].[Member] ([Id], [Name], [HasMessage]) VALUES (2, N'Jerry', 0) 20 SET IDENTITY_INSERT [dbo].[Member] OFF 21 22 CREATE TABLE [dbo].[MemberMessage]( 23 [Id] [int] IDENTITY(1,1) NOT NULL, 24 [Message] [nvarchar](128) NOT NULL, 25 [MemberId] [int] NOT NULL, 26 CONSTRAINT [PK_MemberMessage] PRIMARY KEY CLUSTERED 27 ( 28 [Id] ASC 29 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 30 ) ON [PRIMARY] 31 GO 32 33 ALTER TABLE [dbo].[MemberMessage] WITH CHECK ADD CONSTRAINT [FK_MemberMessage_Member] FOREIGN KEY([MemberId]) 34 REFERENCES [dbo].[Member] ([Id]) 35 GO 36 ALTER TABLE [dbo].[MemberMessage] CHECK CONSTRAINT [FK_MemberMessage_Member] 37 GO
方法1:不使用TransactionScope,只依賴Entity各字段的默認併發控制。測試
Context和Entity定義spa
1 public class MyDbContext : DbContext 2 { 3 public MyDbContext() : base("TransactionTest") { } 4 5 public MyDbContext(string connectionString) : 6 base(connectionString) 7 { 8 9 } 10 11 public DbSet<Member> Members { get; set; } 12 } 13 14 [Table("Member")] 15 public class Member 16 { 17 [Key] 18 public int Id { get; set; } 19 20 public string Name { get; set; } 21 22 public bool HasMessage { get; set; } 23 24 public virtual ICollection<MemberMessage> Messages { get; set; } 25 } 26 27 [Table("MemberMessage")] 28 public class MemberMessage 29 { 30 [Key] 31 public int Id { get; set; } 32 33 public string Message { get; set; } 34 35 public int MemberId { get; set; } 36 37 [ForeignKey("MemberId")] 38 public virtual Member Member { get; set; } 39 }
測試代碼3d
1 try 2 { 3 using (var context = new MyDbContext()) 4 { 5 var tom = context.Members.FirstOrDefault(m => m.Id == 1); 6 if (tom != null && !tom.HasMessage) 7 { 8 Console.WriteLine("Press Enter to Insert MemberMessage..."); 9 Console.ReadLine(); 10 tom.Messages.Add(new MemberMessage() 11 { 12 Message = "Hi Tom!" 13 }); 14 tom.HasMessage = true; 15 context.SaveChanges(); 16 Console.WriteLine("Insert Completed!"); 17 } 18 } 19 } 20 catch (Exception ex) 21 { 22 Console.WriteLine("Insert Failed: " + ex); 23 }
同時運行兩個程序,結果是沒法確保不重複插入
經過分析不難發現,該場景的併發控制關鍵就在於插入前檢查HasMessage若是是False,則插入MemberMessage後更新Member.HasMessage字段時須要再次檢查數據庫中HasMessage字段是否爲False,若是爲True就是有其餘人併發的更改了該字段,本次保存應該回滾或作其餘處理。因此爲此須要有針對性的加入併發控制。
方法2:給HasMessage字段加上併發檢查
1 [Table("Member")] 2 public class Member 3 { 4 [Key] 5 public int Id { get; set; } 6 7 public string Name { get; set; } 8 9 [ConcurrencyCheck] 10 public bool HasMessage { get; set; } 11 12 public virtual ICollection<MemberMessage> Messages { get; set; } 13 }
仍然使用方法1的測試代碼,結果則是其中一次數據插入會拋出OptimisticConcurrencyException,也就是說防止重複插入數據的目的已經達到了。
那回過頭來看看是否可使用TransactionScope對EF進行併發控制,因而有方法3:使用TransactionScope但不給Entity加入併發檢查
Context和Entity的定義與方法1徹底一致,測試代碼爲
1 try 2 { 3 using (var scope = new System.Transactions.TransactionScope()) 4 { 5 using (var context = new MyDbContext()) 6 { 7 var tom = context.Members.FirstOrDefault(m => m.Id == 1); 8 if (tom != null && !tom.HasMessage) 9 { 10 Console.WriteLine("Press Enter to Insert MemberMessage..."); 11 Console.ReadLine(); 12 tom.Messages.Add(new MemberMessage() 13 { 14 Message = "Hi Tom!" 15 }); 16 tom.HasMessage = true; 17 context.SaveChanges(); 18 Console.WriteLine("Insert Completed!"); 19 } 20 } 21 scope.Complete(); 22 } 23 } 24 catch (Exception ex) 25 { 26 Console.WriteLine("Insert Failed: " + ex); 27 }
一樣啓動兩個程序測試,發現其中一次保存操做拋出DbUpdateException,其內部緣由是Transaction死鎖致使該操做被做爲犧牲者。因此看起來也能夠達到併發控制的效果,這種方式的優勢是不須要去仔細辨別業務中哪些操做會致使字段的併發更新衝突,全部的Entity均可以不加ConcurrencyCheck,缺點則是當衝突很少的時候這種死鎖競爭協調與樂觀併發控制相比性能會低些。
最後爲了完備測試各類組合,咱們試一試方法4:既使用TransactionScope,又在HasMessage字段上加入ConcurrencyCheck,Entity代碼參考方法1,測試代碼則參考方法3,結果仍然是TransactionScope檢測到死鎖並選擇其中一個競爭者拋出異常。
結論: