最近在园子里看到一篇关于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检测到死锁并选择其中一个竞争者抛出异常。
结论: