sql-server - 使用 EF Core 2.1 和 SQL Server 进行正确的并发处理

标签 sql-server entity-framework asp.net-web-api concurrency transactions

我目前正在使用 ASP.NET Core Web API 以及 Entity Framework Core 2.1 和 SQL Server 数据库开发 API。该 API 用于从两个账户 A 和 B 转账。考虑到 B 账户是接受付款的账户的性质,因此可能会同时执行大量并发请求。如您所知,如果管理不善,可能会导致某些用户看不到付款到账。

花了几天时间尝试实现并发,我不知道最好的方法是什么。为了简单起见,我创建了一个测试项目来尝试重现此并发问题。

在测试项目中,我有两条路线:request1和request2,每个路线都执行向同一用户的传输,第一个路线的数量为10,第二个路线的数量为20。我放置了一个Thread.sleep( 10000) 第一个如下:

    [HttpGet]
    [Route("request1")]
    public async Task<string> request1()
    {
        using (var transaction = _context.Database.BeginTransaction(System.Data.IsolationLevel.Serializable))
        {
            try
            {
                Wallet w = _context.Wallets.Where(ww => ww.UserId == 1).FirstOrDefault();
                Thread.Sleep(10000);
                w.Amount = w.Amount + 10;
                w.Inserts++;
                _context.Wallets.Update(w);
                _context.SaveChanges();
                transaction.Commit();
            }
            catch (Exception ex)
            {
                transaction.Rollback();
            }
        }
        return "request 1 executed";
    }

    [HttpGet]
    [Route("request2")]
    public async Task<string> request2()
    {
        using (var transaction = _context.Database.BeginTransaction(System.Data.IsolationLevel.Serializable))
        {
            try
            {
                Wallet w = _context.Wallets.Where(ww => ww.UserId == 1).FirstOrDefault();
                w.Amount = w.Amount + 20;
                w.Inserts++;
                _context.Wallets.Update(w);
                _context.SaveChanges();
                transaction.Commit();
            }
            catch (Exception ex)
            {
                transaction.Rollback();
            }
        }
        return "request 2 executed";
    }

在浏览器中执行 request1 和 request2 后,第一个事务回滚,原因是:

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.

我也可以重试交易,但是有没有更好的方法?使用锁?

可序列化,是最隔离的级别,也是成本最高的,如文档中所述:

No other transactions can modify data that has been read by the current transaction until the current transaction completes.

这意味着没有其他事务可以更新已被另一个事务读取的数据,这在这里按预期工作,因为 request2 路由中的更新等待第一个事务(request1)提交。

这里的问题是,一旦当前事务读取了钱包行,我们需要阻止其他事务的读取,为了解决这个问题,我需要使用锁定,以便当 request1 中的第一个 select 语句执行时,需要之后的所有事务等待第一笔交易完成,以便他们可以选择正确的值。由于 EF Core 不支持锁定,我需要直接执行 SQL 查询,因此在选择钱包时我会为当前选择的行添加行锁

//this locks the wallet row with id 1
//and also the default transaction isolation level is enough
Wallet w = _context.Wallets.FromSql("select * from wallets with (XLOCK, ROWLOCK) where id = 1").FirstOrDefault();
Thread.Sleep(10000);
w.Amount = w.Amount + 10;
w.Inserts++;
_context.Wallets.Update(w);
_context.SaveChanges();
transaction.Commit();

现在,即使在执行多个请求之后,这也可以完美工作,所有组合的传输结果都是正确的。除此之外,我使用一个交易表来保存每笔转账的状态,以保存每笔交易的记录,以防出现问题,我能够使用该表计算所有钱包金额。

现在还有其他方法可以做到这一点,例如:

  • 存储过程:但我希望我的逻辑位于应用程序级别
  • 创建同步方法来处理数据库逻辑:这样所有数据库请求都在单个线程中执行,我读过一篇博客文章,建议使用这种方法,但也许我们会使用多个服务器来实现可扩展性

我不知道是不是我搜索得不好,但我找不到使用 Entity Framework Core 处理悲观并发的好 Material ,即使在浏览 Github 时,我看到的大多数代码都没有使用锁定。

这让我想到了我的问题:这是正确的做法吗?

提前干杯并表示感谢。

最佳答案

我对您的建议是捕获DbUpdateConcurrencyException并使用entry.GetDatabaseValues();entry.OriginalValues.SetValues(databaseValues); 进入你的重试逻辑。无需锁定数据库。

这是 EF Core documentation 上的示例页面:

using (var context = new PersonContext())
{
    // Fetch a person from database and change phone number
    var person = context.People.Single(p => p.PersonId == 1);
    person.PhoneNumber = "555-555-5555";

    // Change the person's name in the database to simulate a concurrency conflict
    context.Database.ExecuteSqlCommand(
        "UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 1");

    var saved = false;
    while (!saved)
    {
        try
        {
            // Attempt to save changes to the database
            context.SaveChanges();
            saved = true;
        }
        catch (DbUpdateConcurrencyException ex)
        {
            foreach (var entry in ex.Entries)
            {
                if (entry.Entity is Person)
                {
                    var proposedValues = entry.CurrentValues;
                    var databaseValues = entry.GetDatabaseValues();

                    foreach (var property in proposedValues.Properties)
                    {
                        var proposedValue = proposedValues[property];
                        var databaseValue = databaseValues[property];

                        // TODO: decide which value should be written to database
                        // proposedValues[property] = <value to be saved>;
                    }

                    // Refresh original values to bypass next concurrency check
                    entry.OriginalValues.SetValues(databaseValues);
                }
                else
                {
                    throw new NotSupportedException(
                        "Don't know how to handle concurrency conflicts for "
                        + entry.Metadata.Name);
                }
            }
        }
    }
}

关于sql-server - 使用 EF Core 2.1 和 SQL Server 进行正确的并发处理,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53584812/

相关文章:

sql-server - SQL Azure 与 SQL Server 有何不同?

sql-server - DateTime 与 DateTime2 时间范围差异

c# - 使用 MVC5、Ajax、C# 和 MSSQL Server 级联 DropdownList

c# - Ef Code First 外键失败

asp.net - OData 和 Entity Framework 不同模型映射

c# - Entity Framework - LINQ 的 SQL 命令

c# - 如何过滤列的第一个字符在 EF 和 SQL Server 范围内的行?

iphone - 验证从移动 (iPhone) 应用程序到 ASP.Net Web API 的请求(对我的设计请求反馈)

Azure 虚拟机和 Web API : how set endpoint

asp.net - 如何包含多个子对象?