使用NOLOCK的Entity Framework

148

我该如何在Entity Framework中使用NOLOCK函数?是XML是唯一的方法吗?

10个回答

217
不行,但是你可以启动一个事务并将隔离级别设置为读取未提交。这本质上与NOLOCK相同,但它不是基于每个表进行操作,而是针对事务范围内的所有内容进行操作。
如果这听起来像是你想要的,下面是如何实现的...
//declare the transaction options
var transactionOptions = new System.Transactions.TransactionOptions();
//set it to read uncommited
transactionOptions.IsolationLevel = System.Transactions.IsolationLevel.ReadUncommitted;
//create the transaction scope, passing our options in
using (var transactionScope = new System.Transactions.TransactionScope(
    System.Transactions.TransactionScopeOption.Required, 
    transactionOptions)
)

//declare our context
using (var context = new MyEntityConnection())
{
    //any reads we do here will also read uncomitted data
    //...
    //...
    //don't forget to complete the transaction scope
    transactionScope.Complete();
}

@DoctorJones,你真的需要transactionScope.Complete();吗? 我习惯于大多数与数据库相关的使用范围自动执行此类操作。 - Wjdavis5
此操作导致NotSupportedException异常:不支持在环境事务中注册。 - JsonStatham
@JsonStatham 这是 .Net Core 2.0 中已知的问题 https://github.com/dotnet/corefx/issues/24282 - Doctor Jones
@DoctorJones,所以在 .Net Core 2 中无法执行未提交读取操作吗? - JsonStatham
1
@JsonStatham 已经在 这个 pull request 中被添加,该请求是为了里程碑 2.1.0。 - Doctor Jones
显示剩余8条评论

87

扩展方法可以使这更容易。

public static List<T> ToListReadUncommitted<T>(this IQueryable<T> query)
{
    using (var scope = new TransactionScope(
        TransactionScopeOption.Required, 
        new TransactionOptions() { 
            IsolationLevel = System.Transactions.IsolationLevel.ReadUncommitted }))
    {
        List<T> toReturn = query.ToList();
        scope.Complete();
        return toReturn;
    }
}

public static int CountReadUncommitted<T>(this IQueryable<T> query)
{
    using (var scope = new TransactionScope(
        TransactionScopeOption.Required, 
        new TransactionOptions() { 
            IsolationLevel = System.Transactions.IsolationLevel.ReadUncommitted }))
    {
        int toReturn = query.Count();
        scope.Complete();
        return toReturn;
    }
}

在我的项目中使用这个会导致连接池完全被利用,从而导致异常。我不知道为什么。还有其他人遇到这个问题吗?有什么建议吗? - Ben Tidman
1
没问题,Ben,请不要忘记始终释放你的连接上下文。 - Alexandre
成功缩小问题范围,排除了事务范围作为可能原因。谢谢。这与我构造函数中的一些连接重试有关。 - Ben Tidman
我认为作用域应该是TransactionScopeOption.Suppress。 - CodeGrue
@Alexandre,如果我在另一个ReadCommitted事务中执行此操作会怎样?例如,我生成了一个事务来开始保存数据,但现在我正在查询更多数据,因此在其中生成了一个ReadUncommitted事务?调用这个“Complete”方法也会完成我的外部事务吗?请给予建议:) - Jason Loki Smith

29

如果您需要大量处理事务,请按照以下步骤设置默认事务隔离级别,这种方法比每次启动transactionscope更少干扰:

在创建对象上下文后,在连接上设置默认的事务隔离级别,只需运行此简单命令即可:

this.context.ExecuteStoreCommand("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;");

使用这种技术,我们能够创建一个简单的EF提供程序来为我们创建上下文,并且每次对我们所有的上下文运行此命令,以便我们默认始终处于“读未提交”状态。具体内容请参见:http://msdn.microsoft.com/en-us/library/aa259216(v=sql.80).aspx

2
仅设置事务隔离级别是没有任何效果的。实际上,您需要在事务中运行才能产生任何效果。MSDN文档中关于“读取未提交”状态的说明指出,“以READ UNCOMMITTED级别运行的事务不会发出共享锁定”。这意味着您必须在事务中运行才能获得好处。(摘自http://msdn.microsoft.com/en-gb/library/ms173763.aspx)。您的方法可能不太侵入性,但如果您不使用事务,它将无法实现任何目标。 - Doctor Jones
3
MSDN文档说:“控制连接到SQL Server的Transact-SQL语句的锁定和行版本控制行为。”和“指定语句可以读取其他事务修改但尚未提交的行。”我编写的这个语句影响每个SQL语句,无论它是否在事务中。我不想在网上与他人争辩,但我们在一个大型生产环境中使用此语句,你显然是错的。不要假设,试一试! - Frank.Germain
我已经尝试过它们了。我们的环境负载很高,如果不在这些事务范围内(和匹配的事务)执行查询,就会导致死锁。我的观察是在 SQL 2005 服务器上进行的,所以我不知道行为是否有所改变。因此,我建议您这样做:如果指定了未提交读取隔离级别但仍然遇到死锁,请尝试将查询放在事务中。如果您在不创建事务的情况下没有遇到死锁,那么就可以了。 - Doctor Jones
3
关于Microsoft SQL Server,所有查询本质上都是事务。指定显式事务只是一种将两个或更多语句分组为同一个事务的手段,以便将它们视为一个原子工作单元。 "SET TRANSACTION ISOLATION LEVEL ..."命令影响连接级别属性,并因此影响从该点开始发出的所有SQL语句(对于该连接),除非被查询提示覆盖。这种行为至少在SQL Server 2000之前就已经存在了。 - Solomon Rutzky
5
@DoctorJones - 请查看:http://msdn.microsoft.com/en-us/library/ms173763.aspx 。这里有一个测试。在SSMS中,打开一个查询(#1),并运行:CREATE TABLE ##Test(Col1 INT); BEGIN TRAN; SELECT * FROM ##Test WITH (TABLOCK, XLOCK);。打开另一个查询(#2),并运行:SELECT * FROM ##Test;。由于标签#1中仍然存在使用独占锁的事务,因此SELECT不会返回。取消#2中的SELECT。在标签#2中运行一次SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED。再次在标签#2中只运行SELECT,它将返回。一定要在标签#1中运行ROLLBACK - Solomon Rutzky
作为对此的最后说明,我建议将此作为另一个问题的可能解决方案,以及TransactionScope的想法(均归功于它们各自在这里的答案)。我首先列出了这个想法,因为它更简单,应该产生了期望的效果。用户尝试了这个想法,并且它起作用了。http://stackoverflow.com/questions/26615535/refresh-data-from-stored-procedure/26717601#26717601 - Solomon Rutzky

24

尽管我完全同意使用“Read Uncommitted”事务隔离级别是最好的选择,但有时您被要求使用NOLOCK提示,且没有任何反对理由。

使用Entity Framework 6,您可以像这样实现自己的DbCommandInterceptor:

public class NoLockInterceptor : DbCommandInterceptor
{
    private static readonly Regex _tableAliasRegex = 
        new Regex(@"(?<tableAlias>AS \[Extent\d+\](?! WITH \(NOLOCK\)))", 
            RegexOptions.Multiline | RegexOptions.IgnoreCase);

    [ThreadStatic]
    public static bool SuppressNoLock;

    public override void ScalarExecuting(DbCommand command, 
        DbCommandInterceptionContext<object> interceptionContext)
    {
        if (!SuppressNoLock)
        {
            command.CommandText = 
                _tableAliasRegex.Replace(command.CommandText, "${tableAlias} WITH (NOLOCK)");
        }
    }

    public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        if (!SuppressNoLock)
        {
            command.CommandText = 
                _tableAliasRegex.Replace(command.CommandText, "${tableAlias} WITH (NOLOCK)");
        }
    }
}

有了这个类,你可以在应用程序启动时应用它:

DbInterception.Add(new NoLockInterceptor());

有条件地关闭当前线程中查询添加NOLOCK提示的功能:

NoLockInterceptor.SuppressNoLock = true;

1
我喜欢这个解决方案,尽管我稍微修改了正则表达式为: - Russ
3
为防止在派生表中添加nolock导致错误,应使用正则表达式“(?<tableAlias>] AS [Extent\d+](?! WITH (NOLOCK)))”。 - Russ
将SuppressNoLock设置在线程级别上是一种方便的方法,但很容易忘记取消布尔值的设置。您应该使用返回IDisposable的函数,Dispose方法只需再次将bool设置为false。 另外,ThreadStatic与async/await不太兼容:https://dev59.com/EGcs5IYBdhLWcg3wPxrN - Jaap
或者,如果您更愿意使用ISOLATION LEVEL: “public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) { if (!SuppressNoLock) command.CommandText = $"SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;{Environment.NewLine}{command.CommandText}"; base.ReaderExecuting(command, interceptionContext); }” - Adi
它也在数据库函数中添加了nolock。如何避免函数? - Ivan Lewis
这是一个很棒的解决方案,谢谢! - michaelb

10

Doctor Jones 的答案基础上,使用 PostSharp 进行增强;

首先是 "ReadUncommitedTransactionScopeAttribute"。

[Serializable]
public class ReadUncommitedTransactionScopeAttribute : MethodInterceptionAspect
{
    public override void OnInvoke(MethodInterceptionArgs args)
    {
        //declare the transaction options
        var transactionOptions = new TransactionOptions();
        //set it to read uncommited
        transactionOptions.IsolationLevel = IsolationLevel.ReadUncommitted;
        //create the transaction scope, passing our options in
        using (var transactionScope = new TransactionScope(TransactionScopeOption.Required, transactionOptions))
        {
            //declare our context
            using (var scope = new TransactionScope())
            {
                args.Proceed();
                scope.Complete();
            }
        }
    }
}

那么,每当你需要它时,

    [ReadUncommitedTransactionScope()]
    public static SomeEntities[] GetSomeEntities()
    {
        using (var context = new MyEntityConnection())
        {
            //any reads we do here will also read uncomitted data
            //...
            //...

        }
    }

拦截器可以添加“NOLOCK”是个不错的功能,但是在连接到其他数据库系统(比如Oracle)时将无法使用。


6
为了解决这个问题,我在数据库上创建了一个视图,并在视图的查询中应用了NOLOCK。然后,我将视图视为EF中的表。

5

随着EF6的推出,Microsoft建议使用BeginTransaction()方法。

在EF6+和EF Core中可以使用BeginTransaction代替TransactionScope。

using (var ctx = new ContractDbContext())
using (var transaction = ctx.Database.BeginTransaction(System.Data.IsolationLevel.ReadUncommitted))
{
    //any reads we do here will also read uncommitted data
}

2
不是很准确 - Entity Framework基本上是在实际数据库之上的一个相对严格的层。您的查询是用ESQL - Entity SQL - 进行构建的,首先针对您的实体模型,由于EF支持多个数据库后端,因此您无法直接向后端发送“原生”SQL。
NOLOCK查询提示是SQL Server特定的东西,在其他支持的数据库上不起作用(除非它们也实现了相同的提示 - 我强烈怀疑)。

这个答案已经过时了 - 你可以像其他人提到的那样使用NOLOCK,并且你可以使用Database.ExecuteSqlCommand()或者DbSet<T>.SqlQuery()来执行“本地”的SQL。 - Dunc
1
@Dunc:感谢你的投票 - 顺便说一下:你不应该在任何情况下使用 (NOLOCK) - 参见Bad Habits to kick - putting NOLOCK everywhere - 不建议在任何地方都使用它 - 相反,要避免这样做! - marc_s

1

顶起这个帖子,因为它是在谷歌搜索中排名第一。EF查询结构已经有所改变。这个正则表达式也适用于联接查询。

另外,使用事务级别的read uncommitted可以工作(示例在transactionscope中),但仍然会阻止更新操作。使用表级别的nolock将允许更新继续进行。在执行此操作之前,请非常注意并进行一些关于使用脏读取的影响的研究

(?<tableAlias>((FROM)|(JOIN))\s\[([^\s]+)\]\sAS\s\[([^\s]+)\](?!\sWITH\s\(NOLOCK\)))

你可以使用任何正则表达式在线测试工具(例如regex101.com)来测试此示例

FROM [table1] AS [t]
INNER JOIN [table2] AS [t2] ON ([t].[ID] = [t2].[ID])
INNER JOIN [table3] AS [t3] WITH (NOLOCK) ON ([t].[ID] = [t3].[ID])

我也清理了这个示例,但是没有包括触发器的布尔标志。如果您想要添加,请随意。

public class NoLockInterceptor : DbCommandInterceptor
    {       

        public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
        {           
            
            var finishedresult = base.ReaderExecuting(command.NoLockCommand(), eventData, result);    
            return finishedresult;  
        }

        public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result, CancellationToken cancellationToken = default)
        {
            
            var finishedresult = base.ReaderExecutingAsync(command.NoLockCommand(), eventData, result, cancellationToken);
            return finishedresult; 
        }

        public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<object> result)
        {
            var finishedresult = base.ScalarExecuting(command.NoLockCommand(), eventData, result);
            return finishedresult;
        }

        public override ValueTask<InterceptionResult<object>> ScalarExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<object> result, CancellationToken cancellationToken = default)
        {
            var finishedresult = base.ScalarExecutingAsync(command.NoLockCommand(), eventData, result, cancellationToken);
            return finishedresult;
        }

        
    } 

    public static class DbCommandExtensions
    {
        private static Regex _tableAliasRegex = new Regex(@"(?<tableAlias>((FROM)|(JOIN))\s\[([^\s]+)\]\sAS\s\[([^\s]+)\](?!\sWITH\s\(NOLOCK\)))",
                RegexOptions.Multiline | RegexOptions.IgnoreCase);

        public static DbCommand NoLockCommand(this DbCommand command)        {          

            string updatedCommandText = _tableAliasRegex.Replace(command.CommandText, "${tableAlias} WITH (NOLOCK)");

            command.CommandText = updatedCommandText;
            return command;
        }
    }

1

一种选择是使用存储过程(类似于Ryan提出的视图解决方案),然后从EF执行存储过程。这样,存储过程执行脏读取,而EF只管传输结果。


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