1

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

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

这些序列化事务不仅应该包含在 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 不支持悲观并发。

我的问题是:

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

2 回答 2

2

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

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 的博客,现在看起来不可用。另一种选择是采用乐观锁定。

于 2017-07-26T10:27:00.583 回答
0

该文档指出 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;
    }
}
于 2017-05-11T19:19:59.317 回答