当前位置: 首页 > news >正文

只读查询的“零分配”之路:EF Core + Dapper + MemoryPack 的组合优化

🚀 只读查询的“零分配”之路:EF Core + Dapper + MemoryPack 的组合优化

目标:在只读接口的热路径上,把关键路径上的临时对象与复制降至最低,降低 GC 压力与 p95/p99 尾延迟,提升吞吐。
主线(“三轨并行”)

  1. EF Core:编译查询 + AsNoTracking() + 显式投影(中低 QPS 的默认路径);
  2. Dapper:手写 SQL + 非缓冲流式(buffered:false)+ 扁平 DTO(热点/大结果集);
  3. 序列化MemoryPack + HttpResponse.BodyWriterIBufferWriter<byte>直写;浏览器/通用生态 → 回退 System.Text.Json(配 Source Generator)。

ℹ️ 术语澄清:“零分配”是工程目标,即尽量将关键路径上的中间对象/复制去除或显著降低;受字符串、网络缓冲、运行库内部对象等影响,端到端“绝对零分配”不可达。


📚 目录

  • 🚀 只读查询的“零分配”之路:EF Core + Dapper + MemoryPack 的组合优化
    • 🏗️ 架构鸟瞰(三轨并行总览)
    • 1) 🧭 适用与边界
    • 2) ⚙️ 三轨并行:总体设计
    • 3) 🧩 EF Core:编译查询 + 禁跟踪 + 显式投影
    • 4) 🧵 Dapper:扁平 DTO + **非缓冲**流式(热点/大结果集)
      • 📡 Dapper 非缓冲序列化时序
    • 5) 📦 EF → JSON 流式端点
    • 6) 📤 输出零拷贝优先:**MemoryPack + BodyWriter** 直写(二进制优先,JSON 回退)
    • 🧠 读路径选择决策树
    • 7) 🔎 Hot Path 审计清单(把分配“看得见”)
    • 8) 🧪 基准方法学(BenchmarkDotNet)
    • 9) 🧰 可复现实验模板(最小工程)
    • 10) 🔁 选择与回滚


🏗️ 架构鸟瞰(三轨并行总览)

序列化轨
Dapper 轨
EF Core 轨
EF Core 轨
Dapper 轨
DTO 流
Row 流
PipeWriter.FlushAsync
MemoryPack / JSON 写出
IBufferWriter
Dapper
buffered:false
🗄️ PostgreSQL
EF Core 编译查询
AsNoTracking + 投影
👤 Client
ASP.NET Core Minimal API

1) 🧭 适用与边界

  • 适用:只读 API、列表页、导出、报表快照;一致性级别以“读已提交/快照读”为主。
  • 暂不讨论:复杂对象图(建议显式投影 DTO,避免 Include 拉整图)、强事务读写混合。

2) ⚙️ 三轨并行:总体设计

  • EF Core 轨(可维护)
    AsNoTracking() + 投影 DTO + 编译查询;用 TagWith("hotpath:...") 标注,便于日志/执行计划定位。编译查询把 LINQ 预编译为委托,适合高重复度查询(是否采用以基准评估为准)。

  • Dapper 轨(性能优先)
    稳定 SQL + 扁平 DTO,必要时 buffered:false 非缓冲流式,明显降低大结果集峰值内存(连接在枚举全过程必须保持打开)。

  • 序列化/输出轨
    MemoryPackIBufferWriter<byte>/PipeWriter 直写Accept 不支持时回退 System.Text.Json(建议启用 Source Generation 以减少反射、兼容 AOT/Trim)。对 BodyWriter调用 FlushAsync 才会把缓冲推入响应体。


3) 🧩 EF Core:编译查询 + 禁跟踪 + 显式投影

准则

  1. 编译查询EF.CompileQuery/CompileAsyncQuery 将 LINQ 表达式编译为委托,绕过查询缓存查找,在高重复度场景更优(先做基准)。
  2. 只读禁跟踪AsNoTracking() 是只读查询的常规选择;AsNoTrackingWithIdentityResolution 会在无跟踪下做身份解析(去重相同主键实例),仅在确需语义时使用。
  3. 查询标签TagWith("hotpath:xxx") 写入 SQL 注释,帮助把 LINQ 与生成 SQL/日志对应。

示例

// EF Core 8/9
using Microsoft.EntityFrameworkCore;public sealed record OrderDto(int Id, string No, decimal Amount, DateTime CreatedAt);public static class Queries
{// 编译查询:只读 + 投影 + 限制条数public static readonly Func<AppDbContext, int, IAsyncEnumerable<OrderDto>>GetRecentOrders = EF.CompileAsyncQuery((AppDbContext db, int take) =>db.Orders.TagWith("hotpath:list-orders")   // 起始处标注,便于日志/执行计划定位.AsNoTracking().OrderByDescending(x => x.CreatedAt).Select(x => new OrderDto(x.Id, x.No, x.Amount, x.CreatedAt)).Take(take));
}

4) 🧵 Dapper:扁平 DTO + 非缓冲流式(热点/大结果集)

  • 列顺序与 DTO 对齐,降低映射开销;
  • 非缓冲buffered:false 使结果延迟枚举,结合“边读边写”显著降低峰值内存;务必保证连接在枚举全过程保持打开
  • AOT/裁剪友好:可评估 Dapper.AOT(构建期生成/拦截器),减少运行时反射/发射。

示例(流式 JSON:取消令牌 + 分段 Flush + STJ SourceGen + 统一限量 + 无 RegisterForDispose)

using System.Data;
using System.Text.Json;
using System.Text.Json.Serialization;
using Dapper;public readonly record struct OrderRow(int Id, string No, decimal Amount, DateTime CreatedAt);// System.Text.Json Source Generator 上下文(AOT/性能友好)
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization,WriteIndented = false)]
[JsonSerializable(typeof(OrderRow))]
[JsonSerializable(typeof(OrderRow[]))]
[JsonSerializable(typeof(OrderDto))]
public partial class SourceGenContext : JsonSerializerContext {}app.MapGet("/orders/dapper-json", async (IDbConnection cnn, HttpContext ctx, int take, CancellationToken ct) =>
{take = Math.Clamp(take, 0, 50_000); // 统一限量,防止误用拉爆内存/带宽const string sql = """select id as Id, no as No, amount as Amount, created_at as CreatedAtfrom ordersorder by created_at desclimit @take;""";// 非缓冲=延迟枚举:连接需保持打开(假设由 DI 托管生命周期,不额外注册释放)if (cnn.State != ConnectionState.Open) cnn.Open();ctx.Response.ContentType = "application/json";using var json = new Utf8JsonWriter(ctx.Response.BodyWriter,new JsonWriterOptions { SkipValidation = true });json.WriteStartArray();int counter = 0;foreach (var row in cnn.Query<OrderRow>(sql, new { take }, buffered: false)){if (ct.IsCancellationRequested) break; // 同步非缓冲无法把 CT 传入 DB 命令JsonSerializer.Serialize(json, row, SourceGenContext.Default.OrderRow);// 可选:分段 Flush,降低尾延迟(阈值可根据网络/代理调优;默认不必频繁刷)if ((++counter % 2000) == 0){json.Flush();await ctx.Response.BodyWriter.FlushAsync(ct);}}json.WriteEndArray();json.Flush();                                 // 刷到 PipeWriter 缓冲await ctx.Response.BodyWriter.FlushAsync(ct); // 推到响应体(网络)
});

📡 Dapper 非缓冲序列化时序

ClientASP.NET CoreIDbConnectionPostgreSQLUtf8JsonWriterGET /orders/dapper-jsonOpen()Execute Query (buffered:false)非缓冲=逐行枚举,避免一次性物化RowSerialize(row)loop[rows]writer.Flush() 将数据刷入管道缓冲BodyWriter.FlushAsync() 推到响应体(网络)ClientASP.NET CoreIDbConnectionPostgreSQLUtf8JsonWriter

5) 📦 EF → JSON 流式端点

using System.Text.Json;app.MapGet("/orders/ef-json-stream", async (AppDbContext db, HttpContext ctx, int take, CancellationToken ct) =>
{take = Math.Clamp(take, 0, 50_000);ctx.Response.ContentType = "application/json";using var json = new Utf8JsonWriter(ctx.Response.BodyWriter);json.WriteStartArray();await foreach (var x in Queries.GetRecentOrders(db, take).WithCancellation(ct)){JsonSerializer.Serialize(json, x, SourceGenContext.Default.OrderDto);}json.WriteEndArray();json.Flush();await ctx.Response.BodyWriter.FlushAsync(ct);
});

6) 📤 输出零拷贝优先:MemoryPack + BodyWriter 直写(二进制优先,JSON 回退)

  • MemoryPack:源码生成、AOT 友好,支持直接序列化到 IBufferWriter<byte>/Stream;适合大对象/高 QPS 返回体。
  • ASP.NET CoreHttpResponse.BodyWriterPipeWriter,缓冲写;调用 FlushAsync 控制何时把缓冲写进响应体。
  • 内容协商:约定 Accept: application/x-memorypack 用二进制;否则回退 JSON(建议 STJ Source Generation 以减少反射与 AOT 风险)。MVC 场景可用 MemoryPack 的 Formatter 简化配置。

示例(媒体类型 application/x-memorypack + 统一限量)

using MemoryPack;
using System.Text.Json;[MemoryPackable]
public partial record OrderDto(int Id, string No, decimal Amount, DateTime CreatedAt);app.MapGet("/orders/ef-mpk", async (AppDbContext db, HttpContext ctx, int take, CancellationToken ct) =>
{take = Math.Clamp(take, 0, 50_000);var list = new List<OrderDto>(Math.Min(take, 8192));await foreach (var x in Queries.GetRecentOrders(db, take).WithCancellation(ct))list.Add(x);var accept = ctx.Request.Headers.Accept.ToString();if (accept.Contains("application/x-memorypack", StringComparison.OrdinalIgnoreCase)){ctx.Response.ContentType = "application/x-memorypack";MemoryPackSerializer.Serialize(ctx.Response.BodyWriter, list); // 直写 IBufferWriter<byte>await ctx.Response.BodyWriter.FlushAsync(ct);}else{ctx.Response.ContentType = "application/json";using var json = new Utf8JsonWriter(ctx.Response.BodyWriter);json.WriteStartArray();foreach (var x in list)JsonSerializer.Serialize(json, x, SourceGenContext.Default.OrderDto);json.WriteEndArray();json.Flush();await ctx.Response.BodyWriter.FlushAsync(ct);}
});

🧠 读路径选择决策树

开始
QPS ≥ 1k/s
且查询字段稳定?
EF Core 编译查询
+ AsNoTracking + 投影
结果集 > 100k rows
或 p95 > 200ms?
Dapper 非缓冲 + 扁平 DTO
带宽敏感/内网对接?
MemoryPack + BodyWriter 直写
System.Text.Json (SourceGen)

注:阈值为示例,请依据你的基准与线上指标设团队门槛。


7) 🔎 Hot Path 审计清单(把分配“看得见”)

  • 分配/GC 监控dotnet-counters monitor System.Runtime 观察 Allocation Rate、Gen0/1/2、堆大小。
  • 执行期追踪dotnet-trace collect -- <command> 采集 EventPipe 事件,配 PerfView/SpeedScope 分析 Alloc Stacks/FlameGraph
  • 代码侧:避免中间 ToList()/string.Format/链式 LINQ 隐式分配;JSON 路径用 Utf8JsonWriter(IBufferWriter<byte>);EF 默认 AsNoTracking()AsNoTrackingWithIdentityResolution 仅在确需去重实例时使用。
⚔️ wrk / bombardier
🟦 ASP.NET Core 应用
🧮 dotnet-counters
🧵 dotnet-trace
📊 Allocation Rate/GC Gen
🔧 修复: 去 ToList/启用非缓冲/直写等

8) 🧪 基准方法学(BenchmarkDotNet)

  • 微基准

    • Case A:EF 普通查询 vs 编译查询(高重复度);
    • Case B:EF 投影 DTO vs Dapper(相同字段、相同筛选);
    • Case C:STJ(SourceGen)vs MemoryPack(S/M/L 对象、批量规模)。
  • 采集[MemoryDiagnoser] 输出 Allocated B/Op 与 GC 次数;[Benchmark(Baseline = true)] 设基线。

  • 端到端:配合 wrk/bombardier 进行吞吐/尾延迟对比,旁路跑 dotnet-counters

基准骨架

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Microsoft.EntityFrameworkCore;[MemoryDiagnoser] // 采集分配/GC 指标
public class EfCompiledVsNormal
{private AppDbContext _db = default!;[GlobalSetup]public void Setup() => _db = DbFactory.Create();[Benchmark(Baseline = true)]public async Task<List<OrderDto>> Normal()=> await _db.Orders.AsNoTracking().OrderByDescending(x => x.CreatedAt).Select(x => new OrderDto(x.Id, x.No, x.Amount, x.CreatedAt)).Take(1000).ToListAsync();[Benchmark]public async Task<List<OrderDto>> Compiled(){var list = new List<OrderDto>(1000);await foreach (var x in Queries.GetRecentOrders(_db, 1000))list.Add(x);return list;}
}public static class Program
{public static void Main() => BenchmarkRunner.Run<EfCompiledVsNormal>();
}
🧰 GlobalSetup: 预置 DB/数据
Case A: EF 普通 vs 编译查询
Case B: EF 投影 vs Dapper
Case C: STJ vs MemoryPack
BenchmarkDotNet
MemoryDiagnoser
📈 ops/s, p95/p99, Allocated B/op, GC 次数
🧭 调整热路径与配置

9) 🧰 可复现实验模板(最小工程)

1) 新建工程 & 依赖

dotnet new web -n ZeroAllocRead
cd ZeroAllocRead
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Dapper
dotnet add package MemoryPack
# 可选(AOT/Trim 友好)
dotnet add package Dapper.AOT

2) Docker 起库(PostgreSQL 16)

# docker-compose.yml
services:db:image: postgres:16environment:POSTGRES_PASSWORD: devpassPOSTGRES_USER: devPOSTGRES_DB: demoports: ["5432:5432"]
docker compose up -d

3) 初始化表与数据(psql 示例)

create table orders(id serial primary key,no text not null,amount numeric not null,created_at timestamp not null default now()
);insert into orders(no,amount,created_at)
select 'ORD-'||g, (random()*1000)::numeric, now() - (g||' minutes')::interval
from generate_series(1, 500000) as g;

4) 代码落地

  • Queries.cs(第 3 节);

  • Program.cs 添加第 4、5、6 节的 Minimal API;

  • 配置连接串与 DbContext 注册;

  • 运行:dotnet run

  • 验证:

    • GET /orders/ef-mpk

      • Accept: application/x-memorypack → MemoryPack 二进制;
      • Accept: application/json → JSON(STJ Source Generation)。
    • GET /orders/dapper-json:大分页下观察内存曲线更稳。

    • GET /orders/ef-json-stream:EF 路径的 JSON 流式对照样例。


10) 🔁 选择与回滚

场景首选路径
变更频繁、中低 QPSEF Core(编译查询 + 投影 + AsNoTracking)
热点/字段固定/高 QPSDapper 非缓冲 + 扁平 DTO
服务间/带宽敏感MemoryPack + BodyWriter 直写;公共 API → JSON 回退

风险与回滚

  • AOT/Trim:普通 Dapper 依赖运行时反射/发射;Dapper.AOT 用生成代码替代,更适配 AOT。
  • 身份解析AsNoTrackingWithIdentityResolution 仅在确需时使用。
  • 协议版本化:MemoryPack 输出建议加魔数/版本;MVC 可用 MemoryPack 的 Formatter 简化配置。

文章转载自:

http://OzTGgUeG.jkcnq.cn
http://CEt5wCH5.jkcnq.cn
http://wNuGn6XJ.jkcnq.cn
http://k6OuCZQL.jkcnq.cn
http://99EaHHNF.jkcnq.cn
http://FKkLjVwW.jkcnq.cn
http://RK0MEEDc.jkcnq.cn
http://ng0xtxtn.jkcnq.cn
http://ZxOXaiIA.jkcnq.cn
http://46nqkXpe.jkcnq.cn
http://RwDafPBE.jkcnq.cn
http://kQoxSMqB.jkcnq.cn
http://ZpIfQrqp.jkcnq.cn
http://1HvVy5dF.jkcnq.cn
http://aBKpL0l2.jkcnq.cn
http://jyVbBDze.jkcnq.cn
http://CpTq4e6A.jkcnq.cn
http://rRMecEkW.jkcnq.cn
http://nurOmnIF.jkcnq.cn
http://1NhHr1o8.jkcnq.cn
http://XHLSC6iD.jkcnq.cn
http://PCRPyX6v.jkcnq.cn
http://xxvKyIZ5.jkcnq.cn
http://2T88gSU9.jkcnq.cn
http://DhTVp4WG.jkcnq.cn
http://3dw06Fif.jkcnq.cn
http://kugrOe98.jkcnq.cn
http://3oZTGDR7.jkcnq.cn
http://O0up0zSZ.jkcnq.cn
http://LMeRryng.jkcnq.cn
http://www.dtcms.com/a/379536.html

相关文章:

  • EMC电磁兼容进阶3讲培训:专题三 近场探头和频谱仪在EMC整改中的应用
  • 清理C盘回忆录
  • 对于单链表相关经典算法题:21. 合并两个有序链表及面试题 02.04. 分割链表的解析
  • 【代码随想录day 24】 力扣 78.集合
  • leetcode算法刷题的第三十二天
  • (done) CUDA 和 CPU 性能对比,矩阵加法和矩阵乘法对比
  • 事实上事实上
  • 【左程云算法07】队列和栈-链表数组实现
  • 关于亚马逊账号关联的思考——关于侵权
  • 【硬件-笔试面试题-84】硬件/电子工程师,笔试面试题(知识点:MOS管是损耗有哪些)
  • mybatis vs mybatis-plus
  • 网络诊断和通信中非常重要的工具或协议
  • Mysql主键选取
  • 蓝桥杯嵌入式
  • Python学习——字典和文件
  • urllib的使用
  • AFSim2.9.0学习笔记 —— 4.1、创建项目,以此项目介绍工作中Wizard使用(红方/蓝方武器平台、阵营、更换图标等,多图详细介绍)
  • 机器人驭风而行:低空经济如何开启智能新纪元【科普类】
  • 【论文速读】LLM Compiler:并行函数调用的新范式
  • 【复习】计网每日一题---海明校验码
  • CVPR 2025最佳论文解读|VGGT:Visual Geometry Grounded Transformer
  • 深度学习里的树模型TabNet
  • 洛谷P5250 【深基17.例5】木材仓库 (集合法)详解
  • zsn的作品集
  • 磁共振成像原理(理论)6:自由感应衰减 (Free Induction Decays)
  • 第3节-使用表格数据-CHECK约束
  • 彻底解决Qt中文乱码以及汉字编码的问题(UTF-8/GBK)
  • 【观察】傅建平:迈向“数据强国”,打通数据要素化“任督二脉”的三把钥匙
  • 一些常用的CAPL小功能
  • 当Claude Code失灵,Qwen Code能否成为你的救星?