只在单个ADO.NET事务中更改隔离级别

3
当使用客户端框架,ORM或类似工具构建查询时,如何为单个事务实现不同的隔离级别是最佳方法(这些工具不支持像WITH(NOLOCK)这样的查询提示)?
假设一个应用程序对于一些复杂而耗时的查询使用了ReadUncommitted级别(对相关风险有很好的认识),并且它应该与NHibernate及其查询条件一起运行(或QueryOver / LINQ,只需不使用字符串拼接!)。
NHibernate不支持with(nolock)提示(除非使用原生SQL,在许多情况下目前正在使用)。
因此,为了替换原生SQL字符串及其繁琐的构建代码,我想使用IsolationLevel.ReadUncommitted的事务来替换“with(nolock)”。
但是,即使在提交/回滚后连接仍保持在更改的隔离级别中,以新级别运行。即使在connection.Close()之后,它返回到连接池并重用更改的隔离级别。
我最初注意到这一点,是因为我测试了使用快照隔离级别打开连接并发送简单查询,如果启用数据库快照模式,则禁用Read Uncommitted(通常没有简单地切换到快照)。测试数据库未启用快照模式,因此我收到了异常,并在catch块中将我的UseReadUncommitted变量设置为'true',但稍后从“新的”/重用的连接查询仍然收到相同的异常。
我编写了一个简单的类来包装事务处理,在using块中自动重置.IsolationLevel。Dispose()但是,这似乎会导致两个额外的往返到DB,并且我不确定更改的隔离级别是否可能在某些情况下“幸存”,并影响其他查询。代码在第一次尝试时工作,它适用于纯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();
            }
        }
    }
}

如果隔离级别不匹配,则似乎需要回滚事务。 - usr
主要交易必须在Transaction属性上显式提交,就像任何交易一样,如果不打算回滚。CheckTran仅用于获取默认/当前隔离级别(如果默认匹配所需的隔离级别,则将其用作主要交易)。 - Erik Hart
我不确定当时我在想什么,才会发出那样的评论。 - usr
这是我使用SQL拦截器的解决方案。 https://dev59.com/q3M_5IYBdhLWcg3wn0hO#39518098 - Tola Ch.
1个回答

3
许多ORM不支持(动态)查询提示是一件遗憾的事情。设置隔离级别或编写包装器视图和TVF是常见的解决方法。
但即使在提交/回滚之后,连接仍然保持在更改的隔离级别中,在新级别中运行所有内容。即使在connection.Close()之后,它也会返回到连接池并被重用,具有更改的隔离级别。 是的,这是SQL Server中的设计缺陷,已经在2014年得到修复。 测试数据库禁用了快照模式,因此我遇到了异常。
这正是我发现这个问题的方式。一个令人不安的发现。
您发布的代码通常应该可以工作。正如您所说,它确实需要额外的往返数据库。实际上,恢复旧的隔离级别会导致两次往返。总共,根据级别是否更改,您的代码计算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

希望这个表现良好。这是单向旅行。您需要进行另一次往返以恢复旧水平。
如果您真的热衷于性能,可以使用自己的简单连接池,其中包含已知状态的连接。
或者,每个隔离级别使用一个连接字符串。使用使它们唯一。
如果您仅在RUC或RC下阅读,则无需此方式中的事务。您可能最终只需要一次往返。
我建议您使用这些中最简单的解决方案,只要可以正常工作即可。

请注意,这个问题在 SQL Server 2014 中似乎未被修复!修复被认为是一个 bug(因为它是一种可能会破坏现有功能的行为更改)。 - binki

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接