【EF 译文系列】重试执行策略的局限性(EF 版本至少为 6)
2014-04-20 21:26 Sun.M 阅读(1216) 评论(0) 编辑 收藏 举报原文链接:Limitations with Retrying Execution Strategies (EF6 onwards)
当使用重试执行策略的时候,大体有以下两种局限性:
不支持以流的方式进行查询
默认情况下,EF 6 或以后的版本,都是使用缓冲的方式而不是流的方式进行查询。如果你想使用以流的方式进行查询,可以使用 AsStreaming 方法来改变 LINQ to Entities 的默认查询方式:
using (var db = new BloggingContext()) { var query = (from b in db.Blogs orderby b.Url select b).AsStreaming(); }
重试执行策略不支持以流的方式来进行查询,因为连接中可能会丢失掉部分的返回信息,而 EF 并不能确定哪些是已经正确返回了(数据可能在查询执行后被改变了,返回信息的顺序可能是不一定的,返回信息也可能没有一个唯一标识),所以无法进行有效的重试执行。
不支持用户手动启动的事务
当你配置了重试执行策略时,对于使用事务是有一定限制的。
怎样是被支持的:EF 默认的事务机制
默认情况下,EF 对数据库进行更新操作时,都会自动的建立在一个事务里,你无需额外的做些什么来开启它。
举个例子,下面代码中的 SavaChanges 会自动的执行在一个事务中,当插入数据中的任何一条失败,事务都会回滚,从而不会对数据库进行任何更改,这时上下文中也会有一个状态来允许 SavaChanges 进行重试操作。
using (var db = new BloggingContext()) { db.Blogs.Add(new Site { Url = "http://msdn.com/data/ef" }); db.Blogs.Add(new Site { Url = "http://www.cnblogs.com/prinsun/" }); db.SaveChanges(); }
怎样是不被支持的:用户手动开启的事务
当不使用重试执行策略的时候,用户可以将多个操作包裹到一个事务中。如下面的代码中,在一个事务中包裹了两个 SavaChanges ,当其中任何一个失败,数据库中都不会有任何记录产生。
using (var db = new BloggingContext()) { using (var trn = db.Database.BeginTransaction()) { db.Blogs.Add(new Site { Url = "http://msdn.com/data/ef" }); db.Blogs.Add(new Site { Url = "http://www.cnblogs.com/prinsun/" }); db.SaveChanges(); db.Blogs.Add(new Site { Url = "http://twitter.com/efmagicunicorns" }); db.SaveChanges(); trn.Commit(); } }
当使用重试执行策略的时候,这样的操作是不受支持的。因为 EF 无法知道任何先前的操作,以至于无法去重试它们。比如,如果上诉的第二个 SavaChanges 失败了,EF 没有办法去尝试重新执行第一个 SavaChanges。
变通的方式
挂起执行策略
一种可行的变通方案便是在需要手动开启事务的时候,暂时的挂起重试执行策略。最简单的实现方法便是在执行策略的配置类中加入一个是否挂起的标识,并在执行策略的 lambda 表达式中用该标识做一个判定:
using System.Data.Entity; using System.Data.Entity.Infrastructure; using System.Data.Entity.SqlServer; using System.Runtime.Remoting.Messaging; namespace Demo { public class MyConfiguration : DbConfiguration { public MyConfiguration() { this.SetExecutionStrategy("System.Data.SqlClient", () => SuspendExecutionStrategy ? (IDbExecutionStrategy)new DefaultExecutionStrategy() : new SqlAzureExecutionStrategy()); } public static bool SuspendExecutionStrategy { get { return (bool?)CallContext.LogicalGetData("SuspendExecutionStrategy") ?? false; } set { CallContext.LogicalSetData("SuspendExecutionStrategy", value); } } } }
注意,我们使用了 CallContext 来存储这样的一个标识,这是为了这个值能和调用线程有关,能够安全的支持异步或多线程下的查询(即不会影响到其它线程中的策略执行)。
现在我们可以在需要手动开启事务的时候,使用挂起策略的这个功能了:
using (var db = new BloggingContext()) { MyConfiguration.SuspendExecutionStrategy = true; using (var trn = db.Database.BeginTransaction()) { db.Blogs.Add(new Site { Url = "http://msdn.com/data/ef" }); db.Blogs.Add(new Site { Url = "http://www.cnblogs.com/prinsun/" }); db.SaveChanges(); db.Blogs.Add(new Site { Url = "http://twitter.com/efmagicunicorns" }); db.SaveChanges(); trn.Commit(); } MyConfiguration.SuspendExecutionStrategy = false; }
手动调用执行策略
另外一个选择便是手动的来执行重试策略,我们需要给相应的策略提供重试的执行逻辑,这样它才可以在操作失败时进行有效的重试。为了防止配置中的策略干扰,我们仍然需要将其挂起。
注意,所有的 Context 都需要在重试的代码块中进行实例化,这样才可以确保在每一次的重试操作中所有模型的状态是正确的:
var executionStrategy = new SqlAzureExecutionStrategy(); MyConfiguration.SuspendExecutionStrategy = true; executionStrategy.Execute( () => { using (var db = new BloggingContext()) { using (var trn = db.Database.BeginTransaction()) { db.Blogs.Add(new Site { Url = "http://msdn.com/data/ef" }); db.Blogs.Add(new Site { Url = "http://www.cnblogs.com/prinsun/" }); db.SaveChanges(); db.Blogs.Add(new Site { Url = "http://twitter.com/efmagicunicorns" }); db.SaveChanges(); trn.Commit(); } } }); MyConfiguration.SuspendExecutionStrategy = false;