为什么 Dapper
的 QueryAsync
在高并发下有时比同步还慢
不是
Dapper本身有问题,而是常见误用导致异步收益被抵消。典型场景是:在 ASP.NET Core 中用
Task.Run(() => connection.Query(...))包裹同步方法假装“异步”,这反而增加线程调度开销,吞吐不升反降。
真正有效的异步必须依赖底层驱动的 I/O 异步能力(如 SQL Server 的
SqlConnection对
SqlClient的原生支持),否则只是“伪异步”。
QueryAsync和
ExecuteAsync必须配合
await使用,不能丢弃返回的
Task确保连接字符串含
Asynchronous Processing=true(.NET Framework)或使用
Microsoft.Data.SqlClient(.NET Core/5+ 默认启用) 避免在异步方法中调用
.Result或
.Wait()—— 这会阻塞线程池线程,高并发时迅速耗尽
Dapper
异步查询必须搭配 async/await
链路全异步
从 HTTP 入口到数据库操作,中间任何一环同步阻塞都会让异步失效。比如 Controller 方法声明为
async Task<iactionresult></iactionresult>,但内部调用了某个同步封装的仓储方法,整个请求仍会占用一个线程直到完成。
常见断点位置:日志记录、缓存读写(
IDistributedCache.GetAsync)、DTO 映射(若含同步计算逻辑)。 检查所有
await调用是否真异步 —— 比如
MemoryCache的
GetOrCreateAsync是真异步,但
GetOrCreate是同步的 避免在
using var conn = new SqlConnection(...)块内做耗时 CPU 操作(如大列表
Select投影),应先
await conn.QueryAsync<t>()</t>拿到数据再处理 批量查询慎用
QueryMultipleAsync:它虽支持多结果集,但内部仍是单连接顺序读取,高并发下易成瓶颈;可考虑拆成并行的独立
QueryAsync(注意连接数限制)
连接池与并发数不匹配是性能拐点的主因
Dapper本身无连接管理,完全依赖
SqlConnection的连接池。默认最大连接数是 100,当并发请求数持续超过该值,后续请求会在连接池队列中等待,表现为
System.Data.SqlClient.SqlException: Timeout expired或高延迟。
这不是
Dapper的锅,但容易误判为“Dapper 不行”。真实瓶颈在连接资源,而非 ORM 层。 监控
SqlServer:Connection Pool\Number of current connections性能计数器(Windows)或应用日志中的连接等待时间 必要时调整连接字符串:
Max Pool Size=200;,但需同步评估 SQL Server 的 max worker threads 是否足够 避免长期持有连接:不要把
SqlConnection提升为类字段或单例,每次查询都新建 +
using释放
public class UserRepository
{
private readonly string _connectionString;
<pre class='brush:php;toolbar:false;'>public UserRepository(string connectionString) => _connectionString = connectionString;
public async Task<User> GetByIdAsync(int id)
{
// ✅ 正确:短生命周期连接,真异步 I/O
await using var conn = new SqlConnection(_connectionString);
return await conn.QueryFirstOrDefaultAsync<User>(
"SELECT * FROM Users WHERE Id = @id",
new { id });
}}
高并发下最常被忽略的,是以为换用
QueryAsync就万事大吉,却没验证连接池是否撑得住、调用链是否真异步、驱动是否启用原生异步。这些点卡住一个,性能就上不去。
