我目前正在使用 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/