EF Core 不引入锁,高并发场景 ExecuteSqlRawAsync("UPDATE Users SET Balance = Balance + {0} WHERE UserId = {1}");后如何获取 Updated 后的值? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
drymonfidelia
V2EX    .NET

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

  •  
  •   drymonfidelia 2024-05-15 16:08:15 +08:00 2907 次点击
    这是一个创建于 513 天前的主题,其中的信息可能已经有所发展或是发生改变。
    用于余额变动记录。再查一遍肯定不行,极端情况下一个用户会同时发 10000 个下单请求(客户端随硬件交付,没有升级功能,无法更新),这样不加锁余额变动记录就不准了。加锁的话性能太差了。
    23 条回复    2024-12-26 14:28:02 +08:00
    lujiaxing
        1
    lujiaxing  
       2024-05-15 16:17:47 +08:00
    这个取决于你用什么数据库吧? 跟 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
        2
    drymonfidelia  
    OP
       2024-05-15 16:33:29 +08:00
    @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
        3
    drymonfidelia  
    OP
       2024-05-15 16:34:32 +08:00
    格式被 V 站弄坏了,在 pastebin 上再发一份 https://pastebin.com/bFHrRT9L
    @lujiaxing
    MoYi123
        4
    MoYi123  
       2024-05-15 16:50:45 +08:00
    postgresql 可以 update returning, mysql 好像只能开事务或者写存储过程.
    drymonfidelia
        5
    drymonfidelia  
    OP
       2024-05-15 16:54:27 +08:00
    @MoYi123 pg 这些方面确实厉害,但是这个项目运行好多年了,我刚接手没多久,不敢改太多
    bqn
        6
    bqn  
       2024-05-15 16:55:19 +08:00
    EF 不是有实体追踪嘛,实体设置需要更新的字段,直接保存就好了
    至于并发的问题,你需要在表中设计一个字段,数据存时间戳。查询出来数据,给实体某一些字段赋值,然后进行更新,如果当前数据的时间戳和数据库中的数据时间戳不一致,表示这条数据被操作过了,会触发一个异常的,直接抛出来就好了
    thtznet
        7
    thtznet  
       2024-05-15 17:01:38 +08:00
    高并发一定要队列,不要想着用数据库的事务去代替领域解决业务问题。
    i8086
        9
    i8086  
       2024-05-15 17:09:15 +08:00
    这个下单量有些高,建议用 7 楼方法~
    drymonfidelia
        10
    drymonfidelia  
    OP
       2024-05-15 17:42:55 +08:00 via iPhone
    @bqn 实体追踪没办法在高并发的情况下给一个字段增加值
    drymonfidelia
        11
    drymonfidelia  
    OP
       2024-05-15 17:49:49 +08:00 via iPhone
    @bqn 我这边的情况是一个客户端会 10000 并发下单,不可能给 9900 个订单全抛异常
    cloudzhou
        12
    cloudzhou  
       2024-05-15 17:51:17 +08:00
    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
        13
    drymonfidelia  
    OP
       2024-05-15 17:57:27 +08:00 via iPhone
    @cloudzhou 这个 version 是一个单独字段么?每个查询都要多 update 一个字段,会不会导致性能问题
    cloudzhou
        14
    cloudzhou  
       2024-05-15 18:00:30 +08:00
    @drymonfidelia 只有写,才会 update 阿,查询多一个字段没问题的,问题在于写
    quan01994
        15
    quan01994  
       2024-05-15 18:01:37 +08:00
    如果是 sqlserver , 有 output inserted.Balance
    drymonfidelia
        16
    drymonfidelia  
    OP
       2024-05-15 20:55:22 +08:00
    @quan01994 是 MySQL 。sqlserver bug 好多
    lovelylain
        17
    lovelylain  
       2024-05-15 21:44:33 +08:00 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;
    drymonfidelia
        18
    drymonfidelia  
    OP
       2024-05-15 21:57:34 +08:00
    @lovelylain 这样要写入硬盘,性能会不会比我现在用的 redis 锁还差
    bqn
        19
    bqn  
       2024-05-16 09:27:35 +08:00
    @drymonfidelia 这 10000 都是对同一条数据做操作?上面的处理并发的方式是对一条的数据的操作,另外可做产生这个异常做重试,同 12 楼的做法思想是一致的,https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=data-annotations 这个文章里面也说了对于并发的处理。
    cloudzhou
        20
    cloudzhou  
       2024-05-16 10:33:41 +08:00
    @lovelylain 是的,就是这个意思

    @drymonfidelia 你要事务性,那么不管怎么做,不会比 redis 更好的了
    如果你不要求事务,用 redis lua ,然后结束后日志入库,这种事务性,如果遇到 redis 不可用,就很难了
    drymonfidelia
        21
    drymonfidelia  
    OP
       2024-05-16 11:17:10 +08:00 via iPhone
    @bqn 对,10000 并发 同一条
    forgottencoast
        22
    forgottencoast  
       2024-05-26 20:47:19 +08:00
    我们以前在 ODBC/OLE DB 是这样做的:
    "UPDATE ....;
    SELECT "
    整个作为一个批处理事务脚本发过去。
    niubiman
        23
    niubiman  
       288 天前
    EF 有 fromsql 方法, 如果是在 MySQL 或者 pg 下,自己写 sql, 带上 for update 即可, 如果是 mssql, 设置事务隔离级别为 RR 即可
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2360 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 15:47 PVG 23:47 LAX 08:47 JFK 11:47
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86