【问题标题】:Is EnableRetryOnFailure() valid way for solving database deadlocks? Does it have negative influence on performance?EnableRetryOnFailure() 是解决数据库死锁的有效方法吗?它对性能有负面影响吗?
【发布时间】:2020-12-29 00:23:36
【问题描述】:

所以我遇到了死锁问题,我得到了这个异常:

System.InvalidOperationException: An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure()' to the 'UseSqlServer' call.

我什至做了第二个项目来编写最简单的代码,这会导致同样的问题,它只是几行代码。这真的很基本,您只需要有两个引用相同不同实体的实体。在我看来,这是很常见的事情,EF Core 默认无法处理此问题,并提示使用EnableRetryOnFailure()

我认为有两个或多个实体与同一个实体有关系是很常见的。我们甚至可以在我们的应用程序中拥有类似日志系统的东西,每当有人添加数据时,它就会将记录添加到数据库的日志表中,并且数据总是与某些日志记录相关。或者我们可以保存哪些用户添加了一些数据等。可能性是无穷无尽的。

我的问题是:如果我们有大型应用程序,那么每秒可能有 1000 个用户触发重试,这取决于他们使用导致此死锁的某些操作的频率。 这不会影响性能吗?这最终不会阻塞数据库吗?

重现此问题的简单代码:

为简单起见,它只是包含控制器内部所有代码的 API。数据库由 EF Core 生成(代码优先方法)。它有两个带有端点的控制器,它们接受带有添加到数据库中的数据的 json 文件。每次将文件中的数据添加到数据库(文件名、日期和时间)时,都会保存有关文件的信息。发布一个文件会产生许多包含数据的记录(取决于里面的内容)和一个包含文件名、日期和时间的记录(每个数据记录都有相同的文件引用,因此所有记录都将具有相同的 InputFileId)。

您只需要同时向两个端点发送请求。或者只是将例如 4 个请求同时发送到同一个端点。我在 json 文件中有 2000 个条目,因此更容易触发死锁需要更长的时间。

用户控制器:

[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
    public MyDbContext DbContext { get; }
    public UsersController(MyDbContext dbContext)
    {
        DbContext = dbContext;
    }

    [HttpPost]
    public async Task<IActionResult> Import([FromForm]IFormFile file)
    {
        using var streamReader = new StreamReader(file.OpenReadStream());
        JsonSerializer serializer = new JsonSerializer();

        List<User> users = (List<User>)serializer.Deserialize(streamReader, typeof(List<User>));
        var inputFile = new InputFile
        {
            FileName = file.FileName,
            DateAdded = DateTime.Now
        };

        foreach (var user in users)
        {
            user.InputFile = inputFile;
        }

        await DbContext.AddRangeAsync(users);
        await DbContext.SaveChangesAsync();

        return Ok();
    }
}

产品控制器:

[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    public MyDbContext DbContext { get; }
    public ProductsController(MyDbContext dbContext)
    {
        DbContext = dbContext;
    }

    [HttpPost]
    public async Task<IActionResult> Import([FromForm]IFormFile file)
    {
        using var streamReader = new StreamReader(file.OpenReadStream());
        JsonSerializer serializer = new JsonSerializer();

        List<Product> products = (List<Product>)serializer.Deserialize(streamReader, typeof(List<Product>));
        var inputFile = new InputFile
        {
            FileName = file.FileName,
            DateAdded = DateTime.Now
        };

        foreach (var product in products)
        {
            product.InputFile = inputFile;
        }

        await DbContext.AddRangeAsync(products);
        await DbContext.SaveChangesAsync();

        return Ok();
    }
}

数据库上下文:

public class MyDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }
    public DbSet<User> Users { get; set; }
    public DbSet<InputFile> InputFiles { get; set; }

    public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
    {
    }
}

产品:

public class Product
{
    public long Id { get; set; }
    public string SerialNumber { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public long InputFileId { get; set; }
    public InputFile InputFile { get; set; }
}

用户:

public class User
{
    public long Id { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }
    public long InputFileId { get; set; }
    public InputFile InputFile { get; set; }
}

输入文件:

public class InputFile
{
    public long Id { get; set; }
    public string FileName { get; set; }
    public DateTime DateAdded { get; set; }
}

Startup.cs、ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddDbContext<MyDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
}

【问题讨论】:

    标签: c# sql-server .net-core entity-framework-core


    【解决方案1】:

    这最终不会阻塞数据库吗?

    默认的EnableRetryOnFailure 实现增加了重试之间的等待时间。所以它通常开箱即用,甚至不需要你调整它。

    这不会影响性能吗?

    不,它没有。

    如果我们有大型应用程序,那么每秒可能有 1000 个用户触发重试,具体取决于他们使用导致此死锁的某些操作的频率。

    还要确保将READ_COMMITTED_SNAPSHOT 设置为ON


    有关详细信息,请参阅Connection Resiliency。 另请查看 SqlServerRetryingExecutionStrategyExecutionStrategy 实现。

    【讨论】:

    • “READ_COMMITTED_SNAPSHOT 设置为 ON”是什么意思?
    • 是否可以使用 EnableRetryOnFailure 选项产生重复?
    • @AkmalSalikhov 对于INSERT 操作,这在极少数情况下是可能的,只要您不使用较重的事务级别。在大多数应用程序的实践中,除非这是像银行应用程序那样严重的事情,否则它通常无关紧要(甚至可能根本不会发生),除非与数据库服务器的连接真的很糟糕。对于那些重要的罕见应用程序,您需要测试这些案例以确定。如果需要,您可以随时加倍努力,并在重试时检查之前的 INSERT 是否成功。
    • @AkmalSalikhov 有关重试和事务的更多信息,请参阅官方文档中的Connection Resiliency: Execution strategies and transactions
    • @AkmalSalikhov 有关如何处理我在第一条评论中概述的特定情况的信息(这是在极少数情况下应该能够导致重复条目的唯一情况),请参阅Connection Resiliency: Transaction commit failure and the idempotency issue在官方文档的同一篇文章中。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-30
    • 1970-01-01
    • 1970-01-01
    • 2011-11-12
    • 2014-01-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多