3

使用客户端框架、ORM 或类似构建查询(不支持 WITH(NOLOCK) 等查询提示)时,为单个事务实现不同隔离级别的最佳方法是什么?

想象一个应用程序使用 ReadUncommitted 级别进行许多复杂且长时间运行的查询(非常清楚相关风险),并且它应该使用 NHibernate 及其查询条件(或 QueryOver/LINQ,只是没有字符串连接!)。

NHibernate 不支持 with(nolock) 提示(使用本机 SQL 时除外,目前在许多情况下都使用该提示)。

因此,为了替换原生 SQL 字符串及其繁琐的构建代码,我想使用带有 IsolationLevel.ReadUncommitted 的事务来替换 'with(nolock)'。

但即使在 Commit/Rollback 之后,连接仍保持在更改后的隔离级别,以新级别运行所有内容。即使在 connection.Close() 之后,它也会返回到连接池并以更改的隔离级别重用。

我最初注意到这一点,因为我测试了打开一个具有快照隔离级别的连接并发送一个简单的查询,如果启用了数据库上的快照模式(通常不容易切换到快照),则禁用未提交的读取。测试数据库禁用了快照模式,所以我遇到了一个异常,并在 catch 块中将我的 UseReadUncommitted 变量设置为“true”,但后来来自“新”/重用连接的查询仍然得到相同的异常。

我编写了一个简单的类来将事务处理包装在 using 块中,自动重置 .Dispose() 中的 IsolationLevel。但这似乎会导致两次额外的数据库往返,并且我不确定更改后的隔离级别是否可能在某些情况下“幸存”处置并影响其他查询。该代码在第一次尝试中工作,它用于普通的 ADO.NET 连接/事务(如果好的话,我将为 NHibernate 会话做另一个!)。

有什么建议么?

public class TransactionContainerTempIsolationLevel : IDisposable
{
    public IsolationLevel OldIsolationLevel { get; private set; }

    public IsolationLevel TempIsolationLevel { get; private set; }

    public IDbTransaction Transaction { get; private set; }

    private readonly IDbConnection _conn;

    public TransactionContainerTempIsolationLevel(IDbConnection connection, IsolationLevel tempIsolationLevel)
    {
        _conn = connection;
        LocalIsolationLevel = localIsolationLevel;

        var checkTran = _conn.BeginTransaction();
        if (checkTran.IsolationLevel == tempIsolationLevel)
        {
            Transaction = checkTran;
        }
        else
        {
            OldIsolationLevel = checkTran.IsolationLevel;
            checkTran.Dispose();
            Transaction = _conn.BeginTransaction(tempIsolationLevel);
        }
    }

    public void Dispose()
    {
        Transaction.Dispose();
        if (OldIsolationLevel != TempIsolationLevel)
        {
            using (var restoreTran = _conn.BeginTransaction(OldIsolationLevel))
            {
                restoreTran.Commit();
            }
        }
    }
}
4

1 回答 1

3

许多 ORM 不支持(动态)查询提示这一事实令人遗憾。设置隔离级别或编写包装视图和 TVF 是常见的解决方法。

但即使在 Commit/Rollback 之后,连接仍保持在更改后的隔离级别,以新级别运行所有内容。即使在 connection.Close() 之后,它也会返回到连接池并以更改的隔离级别重用。

是的,这是 2014 年修复的 SQL Server 中的设计缺陷。

测试数据库禁用了快照模式,所以我遇到了异常

这正是我发现这一点的方式。一个令人不安的发现。

您发布的代码应该可以正常工作。正如您所说,它确实需要对数据库进行额外的往返。事实上,恢复旧的隔离级别会导致两次往返。根据级别是否更改,我总共计算您的代码中的 2 或 6 次往返。

我发现在 <2014 年处理隔离级别泄漏的唯一明智的方法是,每次访问数据库时始终使用显式事务。无论如何,在我看来,这在大多数情况下都是一个好主意。您通常需要选择一个隔离级别并提供原子性。如果您可以转换到 SNAPSHOT(我推荐),您可能希望在一个快照事务中运行多个查询,以便所有查询看到相同的数据。

在 >=2014 中,新打开的连接的默认级别是 READ COMMITTED。

我不明白你为什么要恢复旧的隔离级别。您的代码似乎必须处理打开连接时隔离级别是任意的事实。这意味着仅在某些代码路径(不是全部)中恢复到旧级别不会消除防范任意级别的需要。如果您确实在所有路径中恢复了旧级别,那么您不妨在任何地方使用单个事务,而无需任何恢复逻辑。

因此,您可以简单地使用单个事务并让级别泄漏。如果你真的想恢复我会建议这个 T-SQL:

SELECT isolation_level FROM sys.sessions WHERE session_id = @@SPID
SET TRANSACTION ISOLATION LEVEL X
BEGIN TRAN

希望这表现良好。这是单程往返。您需要再往返一次才能恢复旧级别。

如果您真的热衷于性能,您可以将自己的简单连接池与处于已知状态的连接保持在一起。

或者,每个隔离级别使用一个连接字符串。使用 . 使它们独一无二AppName

如果你只是在 RUC 或 RC 下阅读,你甚至不需要这种方式的交易。你可能会结束一次往返。

我建议您使用适合您的最简单的解决方案。

于 2015-01-18T10:32:09.757 回答