1
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; |
2
drymonfidelia OP @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 锁,但是会导致高并发性能变差很多 |
3
drymonfidelia OP |
4
MoYi123 216 天前
postgresql 可以 update returning, mysql 好像只能开事务或者写存储过程.
|
5
drymonfidelia OP @MoYi123 pg 这些方面确实厉害,但是这个项目运行好多年了,我刚接手没多久,不敢改太多
|
6
bqn 216 天前
EF 不是有实体追踪嘛,实体设置需要更新的字段,直接保存就好了
至于并发的问题,你需要在表中设计一个字段,数据存时间戳。查询出来数据,给实体某一些字段赋值,然后进行更新,如果当前数据的时间戳和数据库中的数据时间戳不一致,表示这条数据被操作过了,会触发一个异常的,直接抛出来就好了 |
7
thtznet 216 天前
高并发一定要队列,不要想着用数据库的事务去代替领域解决业务问题。
|
9
i8086 216 天前
这个下单量有些高,建议用 7 楼方法~
|
10
drymonfidelia OP @bqn 实体追踪没办法在高并发的情况下给一个字段增加值
|
11
drymonfidelia OP @bqn 我这边的情况是一个客户端会 10000 并发下单,不可能给 9900 个订单全抛异常
|
12
cloudzhou 216 天前
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 得到最新值 在同一个事务 |
13
drymonfidelia OP @cloudzhou 这个 version 是一个单独字段么?每个查询都要多 update 一个字段,会不会导致性能问题
|
14
cloudzhou 216 天前
@drymonfidelia 只有写,才会 update 阿,查询多一个字段没问题的,问题在于写
|
15
quan01994 216 天前
如果是 sqlserver , 有 output inserted.Balance
|
16
drymonfidelia OP @quan01994 是 MySQL 。sqlserver bug 好多
|
17
lovelylain 216 天前 via Android
@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; |
18
drymonfidelia OP @lovelylain 这样要写入硬盘,性能会不会比我现在用的 redis 锁还差
|
19
bqn 216 天前
@drymonfidelia 这 10000 都是对同一条数据做操作?上面的处理并发的方式是对一条的数据的操作,另外可做产生这个异常做重试,同 12 楼的做法思想是一致的,https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=data-annotations 这个文章里面也说了对于并发的处理。
|
20
cloudzhou 216 天前
@lovelylain 是的,就是这个意思
@drymonfidelia 你要事务性,那么不管怎么做,不会比 redis 更好的了 如果你不要求事务,用 redis lua ,然后结束后日志入库,这种事务性,如果遇到 redis 不可用,就很难了 |
21
drymonfidelia OP @bqn 对,10000 并发 同一条
|
22
forgottencoast 205 天前
我们以前在 ODBC/OLE DB 是这样做的:
"UPDATE ....; SELECT " 整个作为一个批处理事务脚本发过去。 |