首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >接口并发卡死?吃透 Dapper 异步操作,WebAPI 性能直接翻倍

接口并发卡死?吃透 Dapper 异步操作,WebAPI 性能直接翻倍

作者头像
云中小生
发布2026-07-21 13:57:42
发布2026-07-21 13:57:42
250
举报

同步 Dapper 写起来很顺手,但一上 Web API 就得换异步。这篇聊为什么以及怎么用。


为什么不用异步会出事

线程池的大小是有限的。同步调数据库的时候,线程卡在那里等 IO,什么也干不了。100 个并发请求过来,线程池里如果只有 100 个线程,全部堵在数据库等待上——你猜后面进来的请求怎么办?排队,超时,503。

异步的本质是:发起数据库查询后,线程立刻回到池子里接下一个请求;等数据库返回数据了,系统再分配一个线程继续执行。所以同样的线程数,异步能扛的并发量远高于同步。

但异步也有一个雷区,我踩过一次印象非常深。

有个支付回调接口,之前同事写的同步代码,我接手后改成异步。改完上线,第二天中午支付高峰期接口直接卡死,订单全超时。回滚代码一看——我在 Controller 里写了 .Result

代码语言:javascript
复制
// 就这行,高并发下直接死锁
var order = _orderService
    .GetAsync(id)
    .Result;

原因不展开了,简单说是 async/await 的上下文捕获跟 .Result 的阻塞等待冲突,线程互相等就死锁了。

所以第一条铁律:异步就要到底。Controller → Service → Repository 全是 async Task,任何一层都不许用 .Result.Wait()WaitAll()


异步连接和查询

连接用 OpenAsync(),跟同步就这一个区别:

代码语言:javascript
复制
using (IDbConnection db = new SqlConnection(connectionString))
{
    await db.OpenAsync();
    // 后面全异步
}

查询:

代码语言:javascript
复制
// 查列表
var data = await db.QueryAsync<User>("SELECT * FROM Users WHERE Age > @Age", new { Age =  });

// 查单条(最常用)
var user = await db.QueryFirstOrDefaultAsync<User>("SELECT * FROM Users WHERE Id = @Id", new { Id =  });

// 多条件
var result = await db.QueryAsync<User>(
    "SELECT * FROM Users WHERE Name = @Name AND Email = @Email", 
    new { Name = name, Email = email });

跟同步版规则一样:QueryFirstOrDefaultAsync 查不到返回 null,QueryFirstAsync 查不到抛异常,QuerySingleOrDefaultAsync 查到多条抛异常。

增删改用 ExecuteAsync

代码语言:javascript
复制
await db.ExecuteAsync("INSERT INTO Users (Name, Email, Age) VALUES (@Name, @Email, @Age)", model);

await db.ExecuteAsync("UPDATE Users SET Name = @NewName WHERE Id = @Id", 
    new { NewName = "Robert", Id =  });

await db.ExecuteAsync("DELETE FROM Users WHERE Id = @Id", new { Id =  });

联表查询异步版也一样,加个 Async 后缀,splitOn 用法不变。存储过程也是多一行 commandType 参数,跟同步完全一个套路。这两个就不展开贴完整方法签名了,照第一章的同步版加 Async + await 就是:

代码语言:javascript
复制
// 联表
var result = await db.QueryAsync<User, Address, User>(sql,
    (user, addr) => { user.Address = addr; return user; },
    splitOn: "City");

// 存储过程
var user = await db.QueryFirstOrDefaultAsync<User>(
    "GetUserById", new { Id = id },
    commandType: CommandType.StoredProcedure);

异步事务

这是生产环境最重要的场景。比如用户下单,要同时扣库存、生成订单、写流水——三条 SQL 必须同生共死。

流程很直:OpenAsync → BeginTransactionAsync → ExecuteAsync(都带同一个 tran)→ CommitAsync,异常就 RollbackAsync

代码语言:javascript
复制
public async Task BatchSaveAsync()
{
    using IDbConnection db = new SqlConnection(connStr);
    await db.OpenAsync();

    using IDbTransaction tran = await db.BeginTransactionAsync();
    try
    {
        await db.ExecuteAsync("INSERT INTO Users(Name) VALUES (@Name)",
            new { Name = "用户A" }, transaction: tran);
        await db.ExecuteAsync("UPDATE Logs SET Status='Done' WHERE Id=@Id",
            new { Id =  }, transaction: tran);
        await tran.CommitAsync();
    }
    catch (Exception ex)
    {
        await tran.RollbackAsync();
        throw new Exception("事务失败已回滚", ex);
    }
}

还是要提醒一句:事务里每条 SQL 都得传 transaction: tran,漏一条就白搞。还有就是事务里不能新建连接——同一个 tran 绑定的是当前这个 db,换连接就管不到了。


异步开发的几条死线

  1. 不用 .Result.Wait()WaitAll()——死锁的根
  2. 异步全套:OpenAsync → ExecuteAsync → CommitAsync → RollbackAsync,别跟同步混
  3. 事务内所有操作同一个连接,不能新建
  4. async Task 从 Controller 一路传到 Repository,中间不许断
  5. 异步连接也得 using,一样的释放逻辑

异步 Dapper 其实很好记:同步方法名后面加 Async,前面加 await。难的不是写法,是整条调用链上都保持异步,不留阻塞点。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 .NET修仙日记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么不用异步会出事
  • 异步连接和查询
  • 异步事务
  • 异步开发的几条死线
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档