EntityFramework与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来选择覆盖或丢弃失败的操作:
try
{
// Try to save changes, which may cause a conflict.
int num = context.SaveChanges();
Console.WriteLine("No conflicts. " +
num.ToString() + " updates saved.");
}
catch (OptimisticConcurrencyException)
{
// Resolve the concurrency conflict by refreshing the
// object context before re-saving changes.
context.Refresh(RefreshMode.ClientWins, orders); // Save changes.
context.SaveChanges();
Console.WriteLine("OptimisticConcurrencyException "
+ "handled and changes saved");
}
当然除了乐观并发控制我们还可以对冲突特别频繁、冲突解决代价很大的用例进行悲观并发控制。悲观并发基本思想是不让数据被同时离线修改,也就是像源码管理里面“加锁”功能一样,码农甲锁上了这个文件,乙就不能再修改了,这样一来这个文件就不可能发生冲突,悲观并发控制实现的方式比如数据行加IsLocked字段等。
最后为了进行多Context事务,当然还可以混合使用TransactionScope和EF。
好了理论简单介绍完,下面的例子是几种不同的方法对并发控制的效果,需求是每个Member都有个HasMessage字段,初始为False,我们需要给其中一个Member加入唯一一条MemberMessage,并将Member.HasMessage置为True。
建库脚本:
CREATE DATABASE [TransactionTest]
GO USE [TransactionTest]
GO CREATE TABLE [dbo].[Member](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](32) NOT NULL,
[HasMessage] [bit] NOT NULL,
CONSTRAINT [PK_Member] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET IDENTITY_INSERT [dbo].[Member] ON
INSERT [dbo].[Member] ([Id], [Name], [HasMessage]) VALUES (1, N'Tom', 0)
INSERT [dbo].[Member] ([Id], [Name], [HasMessage]) VALUES (2, N'Jerry', 0)
SET IDENTITY_INSERT [dbo].[Member] OFF CREATE TABLE [dbo].[MemberMessage](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Message] [nvarchar](128) NOT NULL,
[MemberId] [int] NOT NULL,
CONSTRAINT [PK_MemberMessage] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO ALTER TABLE [dbo].[MemberMessage] WITH CHECK ADD CONSTRAINT [FK_MemberMessage_Member] FOREIGN KEY([MemberId])
REFERENCES [dbo].[Member] ([Id])
GO
ALTER TABLE [dbo].[MemberMessage] CHECK CONSTRAINT [FK_MemberMessage_Member]
GO
方法1:不使用TransactionScope,只依赖Entity各字段的默认并发控制。
Context和Entity定义
public class MyDbContext : DbContext
{
public MyDbContext() : base("TransactionTest") { } public MyDbContext(string connectionString) :
base(connectionString)
{ } public DbSet<Member> Members { get; set; }
} [Table("Member")]
public class Member
{
[Key]
public int Id { get; set; } public string Name { get; set; } public bool HasMessage { get; set; } public virtual ICollection<MemberMessage> Messages { get; set; }
} [Table("MemberMessage")]
public class MemberMessage
{
[Key]
public int Id { get; set; } public string Message { get; set; } public int MemberId { get; set; } [ForeignKey("MemberId")]
public virtual Member Member { get; set; }
}
测试代码
try
{
using (var context = new MyDbContext())
{
var tom = context.Members.FirstOrDefault(m => m.Id == );
if (tom != null && !tom.HasMessage)
{
Console.WriteLine("Press Enter to Insert MemberMessage...");
Console.ReadLine();
tom.Messages.Add(new MemberMessage()
{
Message = "Hi Tom!"
});
tom.HasMessage = true;
context.SaveChanges();
Console.WriteLine("Insert Completed!");
}
}
}
catch (Exception ex)
{
Console.WriteLine("Insert Failed: " + ex);
}
同时运行两个程序,结果是无法确保不重复插入
通过分析不难发现,该场景的并发控制关键就在于插入前检查HasMessage如果是False,则插入MemberMessage后更新Member.HasMessage字段时需要再次检查数据库中HasMessage字段是否为False,如果为True就是有其他人并发的更改了该字段,本次保存应该回滚或做其他处理。所以为此需要有针对性的加入并发控制。
方法2:给HasMessage字段加上并发检查
[Table("Member")]
public class Member
{
[Key]
public int Id { get; set; } public string Name { get; set; } [ConcurrencyCheck]
public bool HasMessage { get; set; } public virtual ICollection<MemberMessage> Messages { get; set; }
}
仍然使用方法1的测试代码,结果则是其中一次数据插入会抛出OptimisticConcurrencyException,也就是说防止重复插入数据的目的已经达到了。
那回过头来看看是否可以使用TransactionScope对EF进行并发控制,于是有方法3:使用TransactionScope但不给Entity加入并发检查
Context和Entity的定义与方法1完全一致,测试代码为
try
{
using (var scope = new System.Transactions.TransactionScope())
{
using (var context = new MyDbContext())
{
var tom = context.Members.FirstOrDefault(m => m.Id == );
if (tom != null && !tom.HasMessage)
{
Console.WriteLine("Press Enter to Insert MemberMessage...");
Console.ReadLine();
tom.Messages.Add(new MemberMessage()
{
Message = "Hi Tom!"
});
tom.HasMessage = true;
context.SaveChanges();
Console.WriteLine("Insert Completed!");
}
}
scope.Complete();
}
}
catch (Exception ex)
{
Console.WriteLine("Insert Failed: " + ex);
}
同样启动两个程序测试,发现其中一次保存操作抛出DbUpdateException,其内部原因是Transaction死锁导致该操作被作为牺牲者。所以看起来也可以达到并发控制的效果,这种方式的优点是不需要去仔细辨别业务中哪些操作会导致字段的并发更新冲突,所有的Entity都可以不加ConcurrencyCheck,缺点则是当冲突不多的时候这种死锁竞争协调与乐观并发控制相比性能会低些。
最后为了完备测试各种组合,我们试一试方法4:既使用TransactionScope,又在HasMessage字段上加入ConcurrencyCheck,Entity代码参考方法1,测试代码则参考方法3,结果仍然是TransactionScope检测到死锁并选择其中一个竞争者抛出异常。
结论:
- TransactionScope用或不用主要取决于是否需要进行分布式事务
- 即使不需要分布式事务,TransactionScope也可以用于没有精力仔细分析哪些Entity的字段需要进行并发检查的时候
- 如果能够细粒度分析并发场景,则推荐使用EF自带的并发控制机制
EntityFramework与TransactionScope事务和并发控制的更多相关文章
- pgsql事务与并发控制
事务与并发控制 事务的基本的概念和性质 ACID: 原子性:一个事务要么全部执行,要么全部不执行 一致性:执行事务的时候,数据库从一个一致的状态变更到另一个状态 隔离性: 确保在并发执行的时候,每个事 ...
- 深入理解 MySQL ——锁、事务与并发控制
本文首发于vivo互联网技术微信公众号 mp.weixin.qq.com/s/JFSDqI5ya… 作者:张硕 本文对 MySQL 数据库中有关锁.事务及并发控制的知识及其原理做了系统化的介绍和总结, ...
- c#传统SqlTransaction事务和TransactionScope事务
事务有很多种,看了一些关于事务的问题,这里做下笔记····· 事务时单个的工作单位.如果某一事务成功,则在该事务中进行的所有数据更改均会提交,成为数据库中永久的组成部分.若果事务遇到错误,则必须取消或 ...
- Hibernate 事务和并发控制
首先关于Hibernate事务控制,下面是非常权威的资料, https://docs.jboss.org/hibernate/orm/4.0/devguide/en-US/html/ch02.html ...
- [转帖]2019-03-26 发布 深入理解 MySQL ——锁、事务与并发控制
深入理解 MySQL ——锁.事务与并发控制 https://segmentfault.com/a/1190000018658828 太长了 没看完.. 数据库 并发 mysql 639 次阅读 ...
- [转帖]深入理解 MySQL—锁、事务与并发控制
深入理解 MySQL—锁.事务与并发控制 http://www.itpub.net/2019/04/28/1723/ 跟oracle也类似 其实所有的数据库都有相同的机制.. 学习了机制才能够更好的工 ...
- C# TransactionScope 事务类
微软自带的TransactionScope(.Net Framework 2之后)是个好东东,提供的功能也很强大. 首先说说TransactionScope是什么,并能为我们做什么事情.其实看Tran ...
- 深入理解Mysql——锁、事务与并发控制
本文对锁.事务.并发控制做一个总结,看了网上很多文章,描述非常不准确.如有与您观点不一致,欢迎有理有据的拍砖! mysql服务器逻辑架构 每个连接都会在mysql服务端产生一个线程(内部通过线程池管理 ...
- mysql事务和并发控制
谈到事务,首先想到的问题是并发控制.比如两个用户同时操作数据库里面的一张表,一个正在读数据,一个正在删除数据,那么读数据的读出的结果究竟是多少?并发可以提高系统的性能,让多个用户同时操作一份数据,但为 ...
随机推荐
- Python高手之路【六】python基础之字符串格式化
Python的字符串格式化有两种方式: 百分号方式.format方式 百分号的方式相对来说比较老,而format方式则是比较先进的方式,企图替换古老的方式,目前两者并存.[PEP-3101] This ...
- 关于CryptoJS中md5加密以及aes加密的随笔
最近项目中用到了各种加密,其中就包括从没有接触过得aes加密,因此从网上各种查,官方的一种说法: 高级加密标准(英语:Advanced Encryption Standard,缩写:AES),在密码学 ...
- RxJS + Redux + React = Amazing!(译一)
今天,我将Youtube上的<RxJS + Redux + React = Amazing!>翻译(+机译)了下来,以供国内的同学学习,英文听力好的同学可以直接看原版视频: https:/ ...
- Web大前端时代之:HTML5+CSS3入门系列
准备来一波新技术,待续.... Old: 联系源码:https://github.com/dunitian/LoTHTML5 文档下载:https://github.com/dunitian/LoTD ...
- AutoMapper:Unmapped members were found. Review the types and members below. Add a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type
异常处理汇总-后端系列 http://www.cnblogs.com/dunitian/p/4523006.html 应用场景:ViewModel==>Mode映射的时候出错 AutoMappe ...
- CRL快速开发框架系列教程六(分布式缓存解决方案)
本系列目录 CRL快速开发框架系列教程一(Code First数据表不需再关心) CRL快速开发框架系列教程二(基于Lambda表达式查询) CRL快速开发框架系列教程三(更新数据) CRL快速开发框 ...
- 【干货分享】流程DEMO-资产请购单
流程名: 资产请购 业务描述: 流程发起时,会检查预算,如果预算不够,流程必须经过总裁审批,如果预算够用,将发起流程,同时占用相应金额的预算,但撤销流程会释放相应金额的预算. 流程相关文件: 流程 ...
- php安装threads多线程扩展
php5.3或以上,且为线程安全版本.apache和php使用的编译器必须一致.通过phpinfo()查看Thread Safety为enabled则为线程安全版.通过phpinfo()查看Compi ...
- (转载) Linux IO模式及 select、poll、epoll详解
注:本文是对众多博客的学习和总结,可能存在理解错误.请带着怀疑的眼光,同时如果有错误希望能指出. 同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不同的人在不同的上下文下给出的答案 ...
- 第14章 Linux启动管理(1)_系统运行级别
1. CentOS 6.x 启动管理 (1)系统运行级别 ①运行级别 运行级别 含义 0 关机 1 单用户模式,可以想象为Windows的安全模式,主要用于系统修复.(但不是Linux的安全模式) 2 ...