DbUpdateException- 在我捕获所有异常时未处理重复的键错误

Posted

技术标签:

【中文标题】DbUpdateException- 在我捕获所有异常时未处理重复的键错误【英文标题】:DbUpdateException- Duplicate key error is unhanded while i am catching all exceptions 【发布时间】:2019-07-18 00:04:11 【问题描述】:

我有以下方法

public async Task Foo()
    
        try
        

            //Do stuff
            bool inserted = false;
            int tries=0;
            while (!inserted && tries<2)
            
                try
                
                    inserted = await Bar();                        
                
                catch (Exception ex)
                
                    //log ex and continue
                
                finally
                
                  if(!inserted)
                  
                     tries++;
                  
                
            
        
        catch (Exception ex)
        
            //log ex and continue
        
    

public async Task<bool> Bar()
    
        //setup opbject to be inserted to database

        try
        
            //the table can not have auto incrememnt so we read the max value
            objectToBeAdded.id = Context.Set<object>().Max(o => o.id) + 1;
            await Context.Set<object>().AddAsync(objectToBeAdded);
            await Context.SaveChangesAsync();
            return true;
        
        catch (Exception ex) 
            return false;
        
    

代码在多线程环境中运行,每分钟运行多次,因此总是有可能出现以下异常。

Microsoft.EntityFrameworkCore.DbUpdateException:更新条目时出错。有关详细信息,请参阅内部异常。 ---> mysql.Data.MySqlClient.MySqlException:键 'PRIMARY' 的重复条目 'XXXXX' ---> MySql.Data.MySqlClient.MySqlException:键 'PRIMARY' 的重复条目 'XXXXX'

不幸的是,这是一个很难重现的错误,我们的问题是它使应用程序崩溃,而不是重试并继续前进。

我们无法更改表以支持自动递增主键。

编辑:根据要求提供完整的堆栈跟踪

-错误-执行 DbCommand 失败 (8ms) [参数=[@p0='?' (DbType = Int64), @p1='?' (DbType = Boolean), ....., @pN='?' (DbType = Decimal)], CommandType='Text', CommandTimeout='600'] INSERT INTO table (id, col1, ....colN) 值 (@p0, @p1, . ... @pN); -错误- 保存上下文类型“实体”的更改时数据库中发生异常。 Microsoft.EntityFrameworkCore.DbUpdateException:更新条目时出错。有关详细信息,请参阅内部异常。 ---> MySql.Data.MySqlClient.MySqlException:键 'PRIMARY' 的重复条目 'XXXXX' ---> MySql.Data.MySqlClient.MySqlException:键 'PRIMARY' 的重复条目 'XXXXXX' 在 MySqlConnector.Core.ServerSession.TryAsyncContinuation(Task1 task) in C:\.......\mysqlconnector\src\MySqlConnector\Core\ServerSession.cs:line 1248 at System.Threading.Tasks.ContinuationResultTaskFromResultTask2.InnerInvoke() 在 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext,ContextCallback 回调,对象状态) --- 从先前抛出异常的位置结束堆栈跟踪 --- 在 System.Threading.Tasks.Task.ExecuteWithThreadLocal(任务和 currentTaskSlot) --- 从先前抛出异常的位置结束堆栈跟踪 --- 在 C:........\mysqlconnector\src\MySqlConnector\Core\ResultSet.cs:line 42 中的 MySqlConnector.Core.ResultSet.ReadResultSetHeaderAsync(IOBehavior ioBehavior) --- 内部异常堆栈跟踪结束 --- 在 C:........\mysqlconnector\src\MySqlConnector\MySql.Data.MySqlClient\MySqlDataReader.cs: 80 行中的 MySql.Data.MySqlClient.MySqlDataReader.ActivateResultSet(ResultSet resultSet) 在 C:........\mysqlconnector\src\MySqlConnector\MySql.Data.MySqlClient\MySqlDataReader.cs:line 302 中的 MySql.Data.MySqlClient.MySqlDataReader.ReadFirstResultSetAsync(IOBehavior ioBehavior) 在 C:........\mysqlconnector\src\MySqlConnector\MySql.Data.MySqlClient\MySqlDataReader.cs 中的 MySql.Data.MySqlClient.MySqlDataReader.CreateAsync(MySqlCommand command, CommandBehavior behavior, ResultSetProtocol resultSetProtocol, IOBehavior ioBehavior) :第287行 在 C:........\mysqlconnector\src\MySqlConnector\Core\TextCommandExecutor.cs: 中的 MySqlConnector.Core.TextCommandExecutor.ExecuteReaderAsync(String commandText, MySqlParameterCollection parameterCollection, CommandBehavior 行为, IOBehavior ioBehavior, CancellationToken cancelToken) 在 Microsoft.EntityFrameworkCore.Storage.Internal.RelationalCommand.ExecuteAsync(IRelationalConnection 连接,DbCommandMethod 执行方法,IReadOnlyDictionary2 parameterValues, CancellationToken cancellationToken) at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken) --- End of inner exception stack trace --- at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken) at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(DbContext _, ValueTuple2 参数,CancellationToken 取消令牌) 在 Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func4 operation, Func4 verifySucceeded, TState state, CancellationToken cancelToken) 在 Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func4 operation, Func4 verifySucceeded, TState state, CancellationToken cancelToken) 在 Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IReadOnlyList`1 entriesToSave,CancellationToken cancelToken) 在 Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(布尔型 acceptAllChangesOnSuccess,CancellationToken cancelToken) 在 Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancelToken)

【问题讨论】:

可能.. 两个线程获取相同的最大值并尝试将该最大值 + 1 插入/更新到主键中。 @PaulSpiegel 这是给定的。问题是程序因未处理的异常而崩溃。但我们实际上将 saveasync 封装在 3 个 try catch 块中,并且没有一个执行其代码 那么错误可能在catch块中:-) @PaulSpiegel 上面的代码是我们生产代码的匿名版本。甚至完整的堆栈跟踪也显示 SaveAsync 是罪魁祸首。 如果你使用一些硬编码的 id 值,比如objectToBeAdded.id = 1234;。这应该很容易重现,您应该能够调试。 【参考方案1】:

据我所知,您并没有从DbContext 中删除添加的对象,因此重复键仍然存在。

你应该

    要么remove(detach)它 或者每次都从头开始创建一个新的DbContext 以确保安全。

【讨论】:

您已经纠正了我们应该从上下文中删除它,因为每个后续调用 save async 都会尝试将它添加到数据库中。但这并不能解释为什么它没有被任何 try catch 块捕获 好吧,如果没有MCVE(和|或至少完整的堆栈跟踪),很难说这怎么可能发生。虽然为竞争条件制作 MCVE 很困难,但在这种特殊情况下,这并非不可能,因为您可以以总是导致异常的方式同步代码。 P.S.:另外,你只有 2 次重试,这似乎太少了。我至少会制作 10 个,可能在两者之间会有小的随机延迟。 P.S.2:实际上,如果争用的风险太大,我什至建议通过一些消息队列等来序列化此类对象的创建。 关于重试不是一个固定的数字,而是一个环境变量,只是想显示重试逻辑。如果不违反合同中的某种条款,很难提供 MCVE。至于完整的堆栈跟踪,我将对其进行匿名化并将其发布在我的问题中 @AlexanderTalavari 您可以创建一个 MCVE,其中包含一些您尝试插入的虚拟表 MyCar(同时在保存之前有条件地添加一个新实体)并给我们该堆栈跟踪。您在此过程中自己发现问题的可能性非零,并且以这种方式违反合同的可能性为零。不幸的是,制作 MCVE 是一个乏味且没有回报的过程,但它通常非常有效。 我添加了堆栈跟踪。如果这没有帮助,我会考虑添加一个足够接近的 MCVE【参考方案2】:

我想,使这个错误难以重现的原因是,您的代码是异步运行的。 您通过查询数据上下文中的当前最大 id 来获取新的最大 id,反过来,在您查询它之后可能会立即更改它,因为另一个线程也可能创建一个新实体。

您应该做的是在您的Bar()-方法中围绕逻辑放置一个锁定语句。通过这样做,您的第二个插入将在您的第一个插入完成后处理,因此所有项目共享相同的最大 ID。

下面的内容应该会有所帮助,我没有在 Visual Studio 中检查它,如果它真的可以编译,但你明白了。

private object _lockObject = new object();
public async Task<bool> Bar()

   //setup object to be inserted to database

    try
    
        // lock your changes, so they run in a safe order
        lock (_lockObject)
        
            //the table can not have auto incrememnt so we read the max value
            objectToBeAdded.id = Context.Set<object>().Max(o => o.id) + 1;
            await Context.Set<object>().AddAsync(objectToBeAdded);
            await Context.SaveChangesAsync();
        
        return true;
    
    catch (Exception ex) 
        return false;
    

【讨论】:

不幸的是,锁定也不是一种选择。该表可以由其他软件以及同一程序的多个实例更改。我们已经接受可能会发生重复密钥异常。我们想要的是减轻它并且不允许它崩溃我们的整个应用程序。我最新的理论与异步有关 正在实现一个存储过程,它给你最大 id,一个选项?

以上是关于DbUpdateException- 在我捕获所有异常时未处理重复的键错误的主要内容,如果未能解决你的问题,请参考以下文章

添加行时出现 DbUpdateException

MVC EF - System.Data.Entity.Infrastructure.DbUpdateException

在我的 C# 代码中显示错误“前一个 catch 子句已经捕获了这个或超类型 `System.Exception' 的所有异常”

WPF 捕获所有异常

在 socket.io 上从客户端捕获所有事件“发出”

捕获当前线程中的所有异常