最近在园子里看到一篇关于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] 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
View Code

相关文章: