
同步 Dapper 写起来很顺手,但一上 Web API 就得换异步。这篇聊为什么以及怎么用。
线程池的大小是有限的。同步调数据库的时候,线程卡在那里等 IO,什么也干不了。100 个并发请求过来,线程池里如果只有 100 个线程,全部堵在数据库等待上——你猜后面进来的请求怎么办?排队,超时,503。
异步的本质是:发起数据库查询后,线程立刻回到池子里接下一个请求;等数据库返回数据了,系统再分配一个线程继续执行。所以同样的线程数,异步能扛的并发量远高于同步。

但异步也有一个雷区,我踩过一次印象非常深。
有个支付回调接口,之前同事写的同步代码,我接手后改成异步。改完上线,第二天中午支付高峰期接口直接卡死,订单全超时。回滚代码一看——我在 Controller 里写了 .Result:
// 就这行,高并发下直接死锁
var order = _orderService
.GetAsync(id)
.Result;
原因不展开了,简单说是 async/await 的上下文捕获跟 .Result 的阻塞等待冲突,线程互相等就死锁了。
所以第一条铁律:异步就要到底。Controller → Service → Repository 全是 async Task,任何一层都不许用 .Result、.Wait()、WaitAll()。
连接用 OpenAsync(),跟同步就这一个区别:
using (IDbConnection db = new SqlConnection(connectionString))
{
await db.OpenAsync();
// 后面全异步
}
查询:
// 查列表
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:
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 就是:
// 联表
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。

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,换连接就管不到了。
.Result、.Wait()、WaitAll()——死锁的根OpenAsync → ExecuteAsync → CommitAsync → RollbackAsync,别跟同步混async Task 从 Controller 一路传到 Repository,中间不许断using,一样的释放逻辑异步 Dapper 其实很好记:同步方法名后面加 Async,前面加 await。难的不是写法,是整条调用链上都保持异步,不留阻塞点。