【问题标题】:entity framework 6 and pessimistic concurrency实体框架6和悲观并发
【发布时间】:2017-10-10 06:38:05
【问题描述】:

我正在开展一个逐步淘汰旧版应用程序的项目。 在此过程中,作为临时解决方案,我们使用数据库与遗留应用程序集成。

旧版应用程序使用具有可序列化隔离级别的事务。 由于数据库与遗留应用程序的集成,我目前最好使用相同的悲观并发模型和可序列化的隔离级别。

这些序列化事务不仅应该包含在 SaveChanges 语句中,还应该包括一些数据读取。

我是这样做的

  • 围绕我的 DbContext 创建一个带有序列化隔离级别的 transactionScope。
  • 创建 DbContext
  • 做一些阅读
  • 对对象进行一些更改
  • 在 DbContext 上调用 SaveChanges
  • 提交事务范围(从而保存更改)

我认为这会将我的整个读取和写入包装到序列化事务中,然后提交。

我认为这是一种悲观并发的方式。

但是,阅读这篇文章,https://docs.microsoft.com/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/handling-concurrency-with-the-entity-framework-in-an-asp-net-mvc-application 声明 ef 不支持悲观并发。

我的问题是:

  • 答:EF 是否支持我在读写时使用可序列化事务的方式
  • B:将读取和写入包装在一个事务中可以保证我的读取数据在提交事务时不会更改。
  • C:这是一种悲观并发,对吧?

【问题讨论】:

    标签: concurrency entity-framework-6 transactionscope serializable pessimistic-locking


    【解决方案1】:

    实现悲观并发的一种方法是使用这样的方法:

    var options = new TransactionOptions
    {
       IsolationLevel = System.Transactions.IsolationLevel.Serializable,
       Timeout = new TimeSpan(0, 0, 0, 10)
    };
    
    using(var scope = new TransactionScope(TransactionScopeOption.RequiresNew, options))
    { ... stuff here ...}
    

    在 VS2017 中,您似乎必须右键单击 TransactionScope,然后让它为以下内容添加引用:Reference Assemblies\Microsoft\Framework.NETFramework\v4.6.1\System.Transactions.dll

    但是,如果您有两个线程尝试增加同一个计数器,您会发现一个成功,而另一个线程在 10 秒内超时。这样做的原因是当他们继续保存更改时,他们都需要将他们的锁升级为独占,但他们不能,因为其他事务已经在同一行上持有共享锁。 SQL Server 会在一个事务失败一段时间后检测死锁以解决死锁。一个事务失败将释放共享锁,第二个事务将能够将其共享锁升级为独占锁并继续执行。

    解决这种死锁的方法是向数据库提供一个 UPDLOCK 提示,例如:

    private static TestEntity GetFirstEntity(Context context) {
    return context.TestEntities
                  .SqlQuery("SELECT TOP 1 Id, Value FROM TestEntities WITH (UPDLOCK)")
                  .Single();
    }
    

    此代码来自 Ladislav Mrnka 的博客,该博客现在看起来不可用。另一种选择是采用乐观锁定。

    【讨论】:

    • 链接是恶意软件什么的
    • 感谢@Yusha,我已将链接替换为快速摘要。
    【解决方案2】:

    文档指出 EF 没有内置的悲观并发支持。但这并不意味着您不能使用 EF 进行悲观锁定。 所以你可以用 EF 进行悲观锁定!

    配方很简单:

    • 使用事务(不一定可序列化,因为它会导致性能不佳。) - 可以使用 readcommitted...但取决于...
    • 进行更改,调用 dbcontext.savechanges()
    • 请锁定您的表 - 手动执行 T-SQL,或随意使用代码 att。以下。
    • 带有提示的给定 T-SQL 命令将保持该数据库锁定直到给定事务的持续时间。
    • 您需要注意一件事:您加载的实体在您执行锁定时可能已过时,因此应重新获取(重新加载)锁定表中的所有实体。

    我做了很多悲观锁,但乐观锁更好。你不会错的。

    悲观锁定无济于事的典型示例是父子关系,您可能会锁定父子关系并将其视为聚合(因此您假设您也是唯一可以访问子项的人)。因此,如果其他线程试图访问父对象,它将无法工作(将被阻塞),直到其他线程从父表中释放锁。但是使用 ORM,任何其他编码器都可以独立加载子对象 - 从那时起,2 个线程将对子对象进行更改...使用悲观锁定可能会弄乱数据,使用乐观锁定会出现异常,您可以重新加载有效数据并尝试再次保存...

    所以代码:

    public static class DbContextSqlExtensions
    {
        public static void LockTable<Entity>(this DbContext context) where Entity : class
        {
            var tableWithSchema = context.GetTableNameWithSchema<Entity>();
            context.Database.ExecuteSqlCommand(string.Format("SELECT null as dummy FROM {0} WITH (tablockx, holdlock)", tableWithSchema));
        }
    }
    
    public static class DbContextExtensions
    {
        public static string GetTableNameWithSchema<T>(this DbContext context)
                    where T : class
        {
            var entitySet = GetEntitySet<T>(context);
            if (entitySet == null)
                throw new Exception(string.Format("Unable to find entity set '{0}' in edm metadata", typeof(T).Name));
    
            var tableName = GetStringProperty(entitySet, "Schema") + "." + GetStringProperty(entitySet, "Table");
            return tableName;
        }
    
        private static EntitySet GetEntitySet<T>(DbContext context)
        {
            var type = typeof(T);
            var entityName = type.Name;
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
    
            IEnumerable<EntitySet> entitySets;
            entitySets = metadata.GetItemCollection(DataSpace.SSpace)
                             .GetItems<EntityContainer>()
                             .Single()
                             .BaseEntitySets
                             .OfType<EntitySet>()
                             .Where(s => !s.MetadataProperties.Contains("Type")
                                         || s.MetadataProperties["Type"].ToString() == "Tables");
            var entitySet = entitySets.FirstOrDefault(t => t.Name == entityName);
            return entitySet;
        }
    
        private static string GetStringProperty(MetadataItem entitySet, string propertyName)
        {
            MetadataProperty property;
            if (entitySet == null)
                throw new ArgumentNullException("entitySet");
            if (entitySet.MetadataProperties.TryGetValue(propertyName, false, out property))
            {
                string str = null;
                if (((property != null) &&
                    (property.Value != null)) &&
                    (((str = property.Value as string) != null) &&
                    !string.IsNullOrEmpty(str)))
                {
                    return str;
                }
            }
            return string.Empty;
        }
    }
    

    【讨论】:

    • 感谢您回答我的问题。我的印象是可序列化的隔离会为我锁定表。当我进行一些测试时,这似乎也有效。我是否正确理解您是否保存它没有,我需要手动执行。谢谢
    • Serializable 进行锁定。但这是您可以获得的最昂贵的锁定,因此应该避免。在 Web 应用中,如果 10 个以上的用户同时进行可序列化隔离的请求,那么他们将相互等待……如果并发用户超过 5 个,请避免可序列化。或者至少将其限制在特殊用例中!我真的建议您尝试乐观锁定 - 它只需要一个额外的字段。唯一的缺点是您可能会更改需要处理的异常。但是乐观可以和表锁混在一起(我附上的代码)。
    • 感谢您的回答。我了解这种昂贵的隔离级别的缺点。事实上,我更喜欢的方式确实是乐观锁定。就是这样,因为我处于过渡期,一个数据库上有两个应用程序,这只是一个临时解决方案。在我们扼杀旧应用程序之前,我不需要更改锁定级别,只会给项目带来风险。再次感谢您回答我的问题。
    • 嗨@baHI,解决方法看起来不错,但我有疑问,我需要在哪里或何时调用 dbContext.LockTable() ?修改实体之前还是之后?你能分享一些例子吗?提前致谢。
    • @JoaquinAlberto 所以通常需要在加载实体之前锁定,这样可以确保在加载之后直到您保存它之前,没有其他人可以更改实体。但是你需要保持锁的寿命很短。如果您正在使用简单的实体并且没有/最小的关系并且只有一个,则需要锁定主要项目并确保其上有一些原子的东西,这很好。但如果存在要“锁定”的层次结构或多个数据之间的任何依赖关系,最好的方法是使用乐观并发。
    猜你喜欢
    • 2014-04-02
    • 1970-01-01
    • 1970-01-01
    • 2011-02-23
    • 2023-03-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-27
    相关资源
    最近更新 更多