EntityFramework与TransactionScope事务和并发控制 transactionscope原理
最近在园子里看到一篇关于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。
好了理论简单介绍完,下面的例子是几种不同的方法对并发控制的效果,需求是每个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] ASC14 )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 GO17 SET IDENTITY_INSERT [dbo].[Member] ON18 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] OFF21 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] ASC29 )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 GO32 33 ALTER TABLE [dbo].[MemberMessage] WITH CHECK ADD CONSTRAINT [FK_MemberMessage_Member] FOREIGN KEY([MemberId])34 REFERENCES [dbo].[Member] ([Id])35 GO36 ALTER TABLE [dbo].[MemberMessage] CHECK CONSTRAINT [FK_MemberMessage_Member]37 GO
View Code
方法1:不使用TransactionScope,只依赖Entity各字段的默认并发控制。
Context和Entity定义
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 Member16 {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 MemberMessage29 {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 }
View Code
测试代码
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 }
View Code
同时运行两个程序,结果是无法确保不重复插入
通过分析不难发现,该场景的并发控制关键就在于插入前检查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完全一致,测试代码为
360docimg_20_
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 }360docimg_21_
View Code
同样启动两个程序测试,发现其中一次保存操作抛出DbUpdateException,其内部原因是Transaction死锁导致该操作被作为牺牲者。所以看起来也可以达到并发控制的效果,这种方式的优点是不需要去仔细辨别业务中哪些操作会导致字段的并发更新冲突,所有的Entity都可以不加ConcurrencyCheck,缺点则是当冲突不多的时候这种死锁竞争协调与乐观并发控制相比性能会低些。
360docimg_22_
最后为了完备测试各种组合,我们试一试方法4:既使用TransactionScope,又在HasMessage字段上加入ConcurrencyCheck,Entity代码参考方法1,测试代码则参考方法3,结果仍然是TransactionScope检测到死锁并选择其中一个竞争者抛出异常。
360docimg_23_
结论:
TransactionScope用或不用主要取决于是否需要进行分布式事务
即使不需要分布式事务,TransactionScope也可以用于没有精力仔细分析哪些Entity的字段需要进行并发检查的时候
如果能够细粒度分析并发场景,则推荐使用EF自带的并发控制机制
更多阅读
刘忠兴:研究方法与物理学陷入和走出困境的因果关系——物理学基
研究方法与物理学陷入和走出困境的因果关系 ——物理学基础理论批判刘忠兴北京相对论研究联谊会zhongxing_l@sina.com
保和丸的功效与作用 保和丸饭前还是饭后
神曲甘辛性温,消食健胃,长于化酒食陈腐之积;莱菔子辛甘而平,下气消食除胀,长于消谷面之积。三药同用为臣,能消各种食物积滞。食积易于阻气、生湿、化热,故以半夏、陈皮辛温,理气化湿,和胃止呕;茯苓甘淡,健脾利湿,和中止泻;连翘味苦微寒,既可散结以
命令与征服 将军和绝命时刻的注册表文件以及序列号修改器 命令与征服之绝命时刻
一、命令与征服:将军 、命令与征服:绝命时刻 注册表文件。用于解决系统重装后,原先安装的游戏无法运行的问题。把虚线内的文字复制到文本文档,然后将红色部分 改为命令与征服:绝命时刻和命令与征服:将军的安装路径,注意是2条右斜杠。之后将
DDR3和DDR2和DDR的工作原理及技术区别 ddr2与ddr3内存条区别
DDR3和DDR2和DDR的工作原理及技术区别 DDR3和DDR2和DDR的工作原理及技术区别DDR2与DDR的区别(1)DDR的定义:严格的说DDR应该叫DDR SDRAM,人们习惯称为DDR,部分初学者也常看到DDR SDRAM,就认为是SDRAM。DDR SDRAM是Double Data Rate SDRAM的
一、WLAN和WIFI的工作原理是什么?如何架构?及区别?二、阐述蓝 人力资源管理原理阐述
一、WLAN和WIFI的工作原理是什么?如何架构?及区别?WLAN的工作原理WLAN由无线网卡、接入控制器设备(Access Controller,AC)、无线接入点(AccessPoint,AP)、计算机和有关设备组成。下面以最广泛使用的无线网卡为例说明WLAN的工作原理。