Dapper 异步多映射的一次性 SQL 缓存持续增长
Dapper 2.1.89 的异步多映射会在一次调用中重复查询同一缓存,使新条目带上命中数。不断变化的 SQL 文本可能因此逃过零命中回收;先稳定 SQL,确需一次性查询时再局部禁用新增缓存。
异步多映射查询只执行了一次,Dapper 的查询缓存却把它记成已经复用。若 SQL 每次还带着不同注释、字面量或追踪编号,缓存条目会不断增加;即使查询没有返回行,也可能出现这个现象。
Dapper 2.1.89 的 QueryAsync<TFirst, TSecond, TReturn> 路径存在这样的内部重复查找。缓存增长可能推高长期运行进程的内存,不能仅凭进程 OOM 就断定是它,也不能把缓存命中数解释为数据库重复执行次数。
两次查缓存,仍可能只有一次数据库执行
异步入口先用查询身份调用 GetCacheInfo,用于准备命令和参数。执行 reader 后,它又把结果交给共用的 MultiMapImpl;后者再次按同一个身份查缓存。第二次查找会增加 RecordHit,于是刚建立的条目也带上了命中数。
查询身份包含 SQL 文本等信息。只差一段请求编号注释的两条 SQL,也可以成为两个条目。该版本的缓存回收每累计一定数量的新增条目触发,只删除命中数不大于零的项目;这些被内部查找“命中”过的一次性条目便可能留下。
这里描述的是 Dapper 的进程内查询缓存,不是数据库执行计划缓存,也不是应用保存的结果列表。应先从内存分析确认增长对象,再核对调用是否走异步多映射、SQL 身份是否持续变化。只查看数据库当前活动查询,未必能看到已经留在应用里的缓存。
先把变化的数据移出 SQL 文本
能够重复使用的业务查询,应让语句文本稳定,通过参数传值。请求编号不要逐次拼进 SQL 注释,追踪信息可放到应用日志或既有数据库诊断入口。表名、列名不能用普通值参数代替;确实需要动态标识符时,仍要使用项目已有的白名单与引用规则。
不能为了降低缓存条目数量,把本来安全的参数绑定改成字符串拼接。还应检查已有追踪中间件是否在送给 Dapper 之前追加注释,否则只改业务层的 SQL 常量,最终身份仍然会变化。
确实一次性的查询,可局部使用 NoCache
正式 2.1.89 的异步多映射支持传入 CommandDefinition。对于已经确认无需复用缓存的入口,可以保留缓冲语义并禁止新增该查询缓存:
var command = new CommandDefinition(
sql,
parameters,
transaction: transaction,
flags: CommandFlags.Buffered | CommandFlags.NoCache,
cancellationToken: cancellationToken);
var rows = await connection.QueryAsync<Parent, Child, Parent>(
command,
(parent, child) =>
{
parent.Child = child;
return parent;
},
splitOn: "ChildId");
这是现有方法中的调用片段:connection、sql、参数、事务、取消令牌和两个映射类型由应用提供,结果列及 ChildId 分割点须与原查询保持一致。若原来还设置了命令超时或其他行为,应一起保留。这里只给使用缓冲结果的入口增加 NoCache,不能无意改成延迟读取。
NoCache 不会清理已经存在的条目,也不是对整个进程查询缓存设置容量上限。全局调用 PurgeQueryCache() 会同时影响其他正在使用的查询,并带来重新构建映射的成本,不适合作为每次请求结束的清理动作。
用受控的一组不同查询身份观察修改前后的缓存条目数,再以固定 SQL 重复调用作为对照;同时检查返回结果、映射和取消行为。排查记录只保留必要的数量和查询形状,避免导出含业务数据的完整 SQL。内存仍增长时,再分别检查结果持有、连接和 reader 生命周期,不把不同资源的增长都算进查询缓存。


