Red*_*eda 7 sql-server concurrency entity-framework transactions asp.net-web-api
我目前正在使用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";
}
Run Code Online (Sandbox Code Playgroud)
在浏览器中执行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.
Run Code Online (Sandbox Code Playgroud)
我也可以重试交易,但是没有更好的方法吗?使用锁?
可序列化,是最孤立的级别,也是最昂贵的,如文档中所述:
在当前事务完成之前,没有其他事务可以修改当前事务已读取的数据.
这意味着没有其他事务可以更新已由另一个事务读取的数据,因为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();
Run Code Online (Sandbox Code Playgroud)
现在,即使在执行多个请求之后,所有组合的传输结果也是正确的.除此之外,我还使用一个事务表,该事务表保存每个与状态进行的资金转移,以便在出现问题时保留每笔交易的记录,以便能够使用此表计算所有钱包金额.
现在有其他方法可以做到:
我不知道我是否搜索不好,但是我找不到使用Entity Framework Core处理悲观并发的好材料,即使在浏览Github时,我见过的大多数代码都没有使用锁定.
这让我想到了我的问题:这是正确的做法吗?
欢呼并提前谢谢.
Ste*_*ski -5
为什么不在代码中处理并发问题,为什么需要在DB层呢?
您可以有一种方法用给定的值更新给定钱包的值,并且您可以在那里使用简单的锁。像这样:
private readonly object walletLock = new object();
public void UpdateWalletAmount(int userId, int amount)
{
lock (balanceLock)
{
Wallet w = _context.Wallets.Where(ww => ww.UserId == userId).FirstOrDefault();
w.Amount = w.Amount + amount;
w.Inserts++;
_context.Wallets.Update(w);
_context.SaveChanges();
}
}
Run Code Online (Sandbox Code Playgroud)
所以你的方法将如下所示:
[HttpGet]
[Route("request1")]
public async Task<string> request1()
{
try
{
UpdateWalletAmount(1, 10);
}
catch (Exception ex)
{
// log error
}
return "request 1 executed";
}
[HttpGet]
[Route("request2")]
public async Task<string> request2()
{
try
{
UpdateWalletAmount(1, 20);
}
catch (Exception ex)
{
// log error
}
return "request 2 executed";
}
Run Code Online (Sandbox Code Playgroud)
您甚至不需要在这种情况下使用事务。