我目前正在使用ASP.NET Core Web API、Entity Framework Core 2.1和SQL Server数据库开发一个API,用于从两个账户A和B之间进行资金转移。由于B账户是接受付款的账户,所以可能会同时执行许多并发请求。如您所知,如果没有很好地管理,这可能导致某些用户无法看到他们的支付到账。
我花了几天时间尝试实现并发处理,但仍无法确定最佳方法。为了简单起见,我创建了一个测试项目,试图重现这个并发问题。
在测试项目中,我有两个路由:request1和request2,每个路由都向同一用户执行转账操作,一个金额为10美元,另一个金额为20美元。我在第一个路由上放置了一个
在浏览器中执行请求1和请求2后,第一个事务被回滚了,原因是:
现在即使执行多次请求,转账的结果仍然是正确的。此外,我正在使用一个事务表来记录每次转账的状态,以便在发生错误时能够计算出所有钱包的金额。
当然,还有其他做法,比如:
- 存储过程:但我想把逻辑放在应用程序层面。 - 编写同步方法来处理数据库逻辑:这样所有的数据库请求都会在单个线程中执行,我曾经读到一篇博客文章建议使用这种方法,但也许我们会使用多台服务器进行扩展。
我不知道我是否没有搜索到好的材料来处理基于 Entity Framework Core 的悲观并发,即使在浏览 Github 时,我看到的大部分代码也没有使用锁定。
这就带来了我的问题:这是正确的做法吗?
感谢您的耐心阅读。
我花了几天时间尝试实现并发处理,但仍无法确定最佳方法。为了简单起见,我创建了一个测试项目,试图重现这个并发问题。
在测试项目中,我有两个路由: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";
}
在浏览器中执行请求1和请求2后,第一个事务被回滚了,原因是:
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.
我也可以重试交易,但是没有更好的方法吗?使用锁定?
在文档中,Serializable级别是最隔离且最昂贵的:
当前事务完成之前,其他事务无法修改已被当前事务读取的数据。
这意味着没有其他事务可以更新另一个事务读取的数据,这在此处起到了预期的作用,因为请求2路由中的更新等待第一个事务(请求1)提交。
问题在于,一旦当前事务读取了钱包行,我们需要阻止其他事务的读取,为解决问题,我需要使用锁定,以便当请求1中的第一个选择语句执行时,所有事务都需要等待第一个事务完成,以便它们可以选择正确的值。由于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 的悲观并发,即使在浏览 Github 时,我看到的大部分代码也没有使用锁定。
这就带来了我的问题:这是正确的做法吗?
感谢您的耐心阅读。
Serializable
隔离级别。是的,这种行为有点出乎意料,但我不认为您会遇到这种情况(长时间运行的事务后跟短时间事务)。即使在RC下读取您的钱包行时,您也可以通过使用xlock、rowlock、holdlock
实现基本相同的效果,除非EF在幕后超越了自己的能力,否则第二个事务应该会等待第一个锁定完成。我建议设置SQL Profiler跟踪以查看实际命中数据库的内容。 - Roger Wolf