吃透 Dapper 异步操作,WebAPI 性能直接翻倍
 同步 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("SELECT * FROM Users WHERE Age > @Age", new { Age = }); // 查单条(最常用) var user = await db.QueryFirstOrDefaultAsync("SELECT * FROM Users WHERE Id = @Id", new { Id = }); // 多条件 var result = await db.QueryAsync( "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(sql, (user, addr) => { user.Address = addr; return user; }, splitOn: "City"); // 存储过程 var user = await db.QueryFirstOrDefaultAsync( "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`,换连接就管不到了。 --- ## **异步开发的几条死线** 1. 不用 `.Result`、`.Wait()`、`WaitAll()`——死锁的根 1. 异步全套:`OpenAsync → ExecuteAsync → CommitAsync → RollbackAsync`,别跟同步混 1. 事务内所有操作同一个连接,不能新建 1. `async Task` 从 Controller 一路传到 Repository,中间不许断 1. 异步连接也得 `using`,一样的释放逻辑 --- 异步 Dapper 其实很好记:同步方法名后面加 `Async`,前面加 `await`。难的不是写法,是整条调用链上都保持异步,不留阻塞点。