EF Core 不引入锁,高并发场景 ExecuteSqlRawAsync("UPDATE Users SET Balance = Balance + {0} WHERE UserId = {1}");后如何获取 Updated 后的值?

217 天前
 drymonfidelia
用于余额变动记录。再查一遍肯定不行,极端情况下一个用户会同时发 10000 个下单请求(客户端随硬件交付,没有升级功能,无法更新),这样不加锁余额变动记录就不准了。加锁的话性能太差了。
2121 次点击
所在节点    .NET
22 条回复
lujiaxing
217 天前
这个取决于你用什么数据库吧? 跟 EF 好像是没什么关系. 你如果觉得不靠谱你就在外面加个 tranaction. 然后设置数据库隔离等级 RC. 这样你只需要在后面再接一个 SELECT 就 OK 了. 而且你也完全可以先查出 Balance, 然后更新. 更新成功后然后用程序计算出新的 Balance 直接返回.

SELECT Balance FROM Users WHERE UserId = {userId};
拿到之后先不管.

UPDATE Users SET Balance = Balance + {amount} WHERE UserId = {userId};

然后 return balance + amount;
drymonfidelia
217 天前
@lujiaxing 我之前就是这么实现的,然后忘记出现了高并发频繁事务失败还是余额加错的问题,代码大致是这样的
var strategy = dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
using (var transaction = await dbContext.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted))
{
var user = await dbContext.Users.NotCacheable().FirstOrDefaultAsync(x => x.UserId == userId);
if (user == null) return;
await dbContext.Entry(user).ReloadAsync();
if (balanceChange < 0 && user.Balance < balanceChange * -1) throw new Exception("UserBalanceNotEnough");
await dbContext.Database.ExecuteSqlRawAsync(
"UPDATE Users SET Balance = Balance + {0} WHERE UserId = {1}",
new object[] { balanceChange, userId });
var balanceRecord = new BalanceRecord
{
UserId = userId,
Description = description,
OperatorIp = operatorIp,
BalanceChange = balanceChange,
RemainingBalance = user.Balance + balanceChange
};
await dbContext.BalanceRecords.AddAsync(balanceRecord);
try
{
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
}
catch (Exception ex)
{
await transaction.RollbackAsync();
}
}
}
改了好几个版本,最后只能又套了一个 Redis 锁,但是会导致高并发性能变差很多
drymonfidelia
217 天前
格式被 V 站弄坏了,在 pastebin 上再发一份 https://pastebin.com/bFHrRT9L
@lujiaxing
MoYi123
217 天前
postgresql 可以 update returning, mysql 好像只能开事务或者写存储过程.
drymonfidelia
217 天前
@MoYi123 pg 这些方面确实厉害,但是这个项目运行好多年了,我刚接手没多久,不敢改太多
bqn
217 天前
EF 不是有实体追踪嘛,实体设置需要更新的字段,直接保存就好了
至于并发的问题,你需要在表中设计一个字段,数据存时间戳。查询出来数据,给实体某一些字段赋值,然后进行更新,如果当前数据的时间戳和数据库中的数据时间戳不一致,表示这条数据被操作过了,会触发一个异常的,直接抛出来就好了
thtznet
217 天前
高并发一定要队列,不要想着用数据库的事务去代替领域解决业务问题。
bqn
217 天前
i8086
217 天前
这个下单量有些高,建议用 7 楼方法~
drymonfidelia
217 天前
@bqn 实体追踪没办法在高并发的情况下给一个字段增加值
drymonfidelia
217 天前
@bqn 我这边的情况是一个客户端会 10000 并发下单,不可能给 9900 个订单全抛异常
cloudzhou
217 天前
1. 存储过程
update 和返回最新值

---
2. 引入 version ,乐观锁自旋
2.1 select version;
2.2 update version=version+1 where version = {old_version}

如果 update 成功,说明 select -》 update 之间没有修改,update 成功,新旧值
如果 失败,重复 2.1-2.2 并引入随机等待

---
3. select * for update 提前加锁
然后 UPDATE Users SET Balance = Balance + {0} WHERE UserId = {1}
再次 select 得到最新值
在同一个事务
drymonfidelia
217 天前
@cloudzhou 这个 version 是一个单独字段么?每个查询都要多 update 一个字段,会不会导致性能问题
cloudzhou
217 天前
@drymonfidelia 只有写,才会 update 阿,查询多一个字段没问题的,问题在于写
quan01994
217 天前
如果是 sqlserver , 有 output inserted.Balance
drymonfidelia
216 天前
@quan01994 是 MySQL 。sqlserver bug 好多
lovelylain
216 天前
@drymonfidelia #13
SELECT Balance, version FROM Users WHERE UserId = {userId};
UPDATE Users SET Balance = Balance + {amount}, version=version+1 WHERE UserId = {userId} AND version={version};
成功 return Balance + amount;
drymonfidelia
216 天前
@lovelylain 这样要写入硬盘,性能会不会比我现在用的 redis 锁还差
bqn
216 天前
@drymonfidelia 这 10000 都是对同一条数据做操作?上面的处理并发的方式是对一条的数据的操作,另外可做产生这个异常做重试,同 12 楼的做法思想是一致的,https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=data-annotations 这个文章里面也说了对于并发的处理。
cloudzhou
216 天前
@lovelylain 是的,就是这个意思

@drymonfidelia 你要事务性,那么不管怎么做,不会比 redis 更好的了
如果你不要求事务,用 redis lua ,然后结束后日志入库,这种事务性,如果遇到 redis 不可用,就很难了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1041031

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX