序列化性能对比与调优
大约 10 分钟约 3125 字
序列化性能对比与调优
简介
ASP.NET Core 默认使用 System.Text.Json 进行 JSON 序列化。理解不同序列化库的性能特征、配置选项和优化策略,有助于在高吞吐场景下选择正确的方案。
特点
System.Text.Json
配置与性能
// JsonSerializerOptions 影响性能的配置
var options = new JsonSerializerOptions
{
// 性能相关
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // 命名策略
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, // 忽略 null
WriteIndented = false, // 不缩进(性能更好)
ReadCommentHandling = JsonCommentHandling.Skip, // 跳过注释
AllowTrailingCommas = true, // 允许尾随逗号
// 安全相关
MaxDepth = 64, // 最大深度
ReferenceHandler = ReferenceHandler.IgnoreCycles, // 循环引用处理
};
// ⚠️ 性能陷阱:每次 new JsonSerializerOptions
// ❌ 每次序列化都创建新 Options
string json = JsonSerializer.Serialize(data, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
// ✅ 复用 Options 实例(内部缓存元数据)
private static readonly JsonSerializerOptions _options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
string json = JsonSerializer.Serialize(data, _options);
// ASP.NET Core 全局配置
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});属性控制
public class Product
{
[JsonPropertyName("id")] // 自定义 JSON 属性名
public int Id { get; set; }
[JsonPropertyName("name")]
[MaxLength(100)]
public string Name { get; set; } = "";
[JsonPropertyName("price")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] // 默认值时不序列化
public decimal Price { get; set; }
[JsonPropertyName("created_at")]
[JsonConverter(typeof(JsonDateTimeConverter))] // 自定义转换器
public DateTime CreatedAt { get; set; }
[JsonPropertyName("tags")]
public List<string> Tags { get; set; } = new();
[JsonIgnore] // 完全忽略
public string InternalField { get; set; } = "";
[JsonPropertyOrder(1)] // 控制序列化顺序
public string? Description { get; set; }
}
// 自定义转换器
public class JsonDateTimeConverter : JsonConverter<DateTime>
{
private const string Format = "yyyy-MM-dd HH:mm:ss";
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> DateTime.ParseExact(reader.GetString()!, Format, CultureInfo.InvariantCulture);
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString(Format, CultureInfo.InvariantCulture));
}Source Generator
编译时序列化生成
// System.Text.Json Source Generator(.NET 6+)
// 编译时生成序列化代码,避免运行时反射
// 1. 定义 Context
[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(List<Product>))]
[JsonSerializable(typeof(Order))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false)]
public partial class AppJsonContext : JsonSerializerContext { }
// 2. 使用生成的代码
// 编译时生成,零反射开销
string json = JsonSerializer.Serialize(product, AppJsonContext.Default.Product);
Product? parsed = JsonSerializer.Deserialize(json, AppJsonContext.Default.Product);
// 3. ASP.NET Core 集成
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolver = AppJsonContext.Default;
});
// 4. 性能对比
// 反射模式: ~2.5μs/次 (首次 ~50μs)
// Source Gen: ~1.2μs/次 (首次 ~1.5μs)
// 性能提升约 2x,首次调用提升 30x+
// 5. Native AOT 兼容
// Source Generator 生成的代码与 AOT 兼容
// 反射模式在 AOT 下不可用序列化库对比
性能基准
// 常见序列化库吞吐量对比( ops/ms,越大越好)
// 数据:简单对象 {"Id":1,"Name":"Test","Price":9.99}
//
// | 库 | 序列化 | 反序列化 | 大小(bytes) |
// |------------------------|----------|----------|------------|
// | System.Text.Json | ~800 | ~700 | ~45 |
// | System.Text.Json (SG) | ~1200 | ~1000 | ~45 |
// | Newtonsoft.Json | ~350 | ~300 | ~50 |
// | MessagePack | ~2000 | ~1800 | ~25 |
// | protobuf-net | ~2500 | ~2200 | ~18 |
// | SpanJson | ~1500 | ~1300 | ~45 |
//
// 结论:
// - 文本格式:System.Text.Json Source Gen 最快
// - 二进制格式:protobuf 最快且最小
// - Newtonsoft 兼容性最好但最慢
// MessagePack 示例
// dotnet add package MessagePack
[MessagePackObject]
public class ProductMsgPack
{
[Key(0)]
public int Id { get; set; }
[Key(1)]
public string Name { get; set; } = "";
[Key(2)]
public decimal Price { get; set; }
}
var bytes = MessagePackSerializer.Serialize(product);
var parsed = MessagePackSerializer.Deserialize<ProductMsgPack>(bytes);
// protobuf-net 示例
// dotnet add package protobuf-net
[ProtoContract]
public class ProductProto
{
[ProtoMember(1)]
public int Id { get; set; }
[ProtoMember(2)]
public string Name { get; set; } = "";
[ProtoMember(3)]
public decimal Price { get; set; }
}
using var ms = new MemoryStream();
Serializer.Serialize(ms, product);
ms.Position = 0;
var parsed2 = Serializer.Deserialize<ProductProto>(ms);性能调优策略
减少序列化开销
// 1. 避免不必要的序列化
// ❌ 返回整个实体(包含不需要的字段)
app.MapGet("/users", (AppDbContext db) =>
db.Users.Select(u => u)); // 序列化所有字段
// ✅ 只返回需要的字段(DTO 投影)
app.MapGet("/users", (AppDbContext db) =>
db.Users.Select(u => new UserDto(u.Id, u.Name, u.Email))); // 只序列化3个字段
// 2. 使用 Utf8JsonWriter 直接写入(最高性能)
app.MapGet("/api/fast", (HttpContext context) =>
{
context.Response.ContentType = "application/json";
using var writer = new Utf8JsonWriter(context.Response.Body);
writer.WriteStartObject();
writer.WriteString("status"u8, "ok");
writer.WriteNumber("count"u8, 42);
writer.WriteEndObject();
});
// 3. 缓存序列化结果(不频繁变化的数据)
public class CachedJsonMiddleware
{
private byte[]? _cachedJson;
private readonly object _lock = new();
public byte[] GetOrSerialize<T>(T data, JsonSerializerOptions options)
{
if (_cachedJson != null) return _cachedJson;
lock (_lock)
{
_cachedJson ??= JsonSerializer.SerializeToUtf8Bytes(data, options);
}
return _cachedJson;
}
}
// 4. 流式序列化(大数据集合)
app.MapGet("/api/users/stream", async (HttpContext context, AppDbContext db) =>
{
context.Response.ContentType = "application/json";
await context.Response.WriteAsync("[", context.RequestAborted);
var first = true;
await foreach (var user in db.Users.AsNoTracking().AsAsyncEnumerable())
{
if (!first) await context.Response.WriteAsync(",", context.RequestAborted);
first = false;
var json = JsonSerializer.Serialize(user);
await context.Response.WriteAsync(json, context.RequestAborted);
}
await context.Response.WriteAsync("]", context.RequestAborted);
});
// 5. 使用 JsonDocument 解析(只读场景)
using var doc = JsonDocument.Parse(jsonString);
var root = doc.RootElement;
var name = root.GetProperty("name").GetString();
var age = root.GetProperty("age").GetInt32();
// 不反序列化为对象,直接读取值循环引用与复杂对象
// 循环引用处理
public class Category
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category? Parent { get; set; } // 父分类
public List<Category> Children { get; set; } = new(); // 子分类
public List<Product> Products { get; set; } = new();
}
// 方式1:忽略循环引用(简单但可能丢数据)
var options = new JsonSerializerOptions
{
ReferenceHandler = ReferenceHandler.IgnoreCycles
};
// 输出: {"Id":1,"Name":"电子","Parent":null,"Children":[{"Id":2,...}]}
// 方式2:保留引用(完整但格式复杂)
var options2 = new JsonSerializerOptions
{
ReferenceHandler = ReferenceHandler.Preserve
};
// 输出: {"$id":"1","Id":1,"Name":"电子","Parent":{"$ref":"2"},...}
// 方式3:使用 DTO 打破循环(推荐)
public class CategoryTreeDto
{
public int Id { get; set; }
public string Name { get; set; } = "";
public int? ParentId { get; set; } // 只保留 ID,不序列化对象
public List<CategoryTreeDto> Children { get; set; } = new();
public int ProductCount { get; set; } // 只保留数量,不序列化列表
}
// 映射方法
public static CategoryTreeDto ToTreeDto(Category category)
{
return new CategoryTreeDto
{
Id = category.Id,
Name = category.Name,
ParentId = category.Parent?.Id,
Children = category.Children.Select(ToTreeDto).ToList(),
ProductCount = category.Products.Count
};
}自定义转换器进阶
// 多态序列化 — 基类引用需要序列化为实际类型
public abstract class PaymentResult
{
public string TransactionId { get; set; } = "";
public decimal Amount { get; set; }
}
public class AlipayResult : PaymentResult
{
public string TradeNo { get; set; } = "";
}
public class WechatResult : PaymentResult
{
public string OpenId { get; set; } = "";
}
// 多态转换器
public class PaymentResultConverter : JsonConverter<PaymentResult>
{
private static readonly Dictionary<string, Type> TypeMap = new()
{
["alipay"] = typeof(AlipayResult),
["wechat"] = typeof(WechatResult)
};
public override PaymentResult? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var doc = JsonDocument.ParseValue(ref reader);
var typeDiscriminator = doc.RootElement.GetProperty("type").GetString();
if (typeDiscriminator != null && TypeMap.TryGetValue(typeDiscriminator, out var targetType))
{
return (PaymentResult?)JsonSerializer.Deserialize(doc.RootElement.GetRawText(), targetType, options);
}
throw new JsonException($"Unknown payment type: {typeDiscriminator}");
}
public override void Write(Utf8JsonWriter writer, PaymentResult value, JsonSerializerOptions options)
{
var typeDiscriminator = value switch
{
AlipayResult => "alipay",
WechatResult => "wechat",
_ => throw new JsonException($"Unknown payment type: {value.GetType().Name}")
};
writer.WriteStartObject();
writer.WriteString("type", typeDiscriminator);
// 序列化具体属性
var json = JsonSerializer.SerializeToElement(value, value.GetType(), options);
foreach (var property in json.EnumerateObject())
{
property.WriteTo(writer);
}
writer.WriteEndObject();
}
}
// 枚举转换器 — 支持字符串和数字互转
public class FlexibleEnumConverter<T> : JsonConverter<T> where T : struct, Enum
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
return Enum.Parse<T>(reader.GetString()!, ignoreCase: true);
}
if (reader.TokenType == JsonTokenType.Number)
{
return (T)(object)reader.GetInt32();
}
throw new JsonException($"Unexpected token type: {reader.TokenType}");
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
// Nullable 时间转换器 — 处理 null 和默认值
public class NullableDateTimeConverter : JsonConverter<DateTime?>
{
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null) return null;
if (reader.TokenType == JsonTokenType.String)
{
var str = reader.GetString();
return string.IsNullOrEmpty(str) ? null : DateTime.Parse(str);
}
return reader.GetDateTime();
}
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
{
if (value.HasValue)
writer.WriteStringValue(value.Value.ToString("yyyy-MM-dd HH:mm:ss"));
else
writer.WriteNullValue();
}
}大文件与流式处理
// 大 JSON 文件流式处理 — 避免一次性加载到内存
public async Task ProcessLargeJsonFileAsync(string filePath, Action<JsonElement> processItem)
{
await using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
using var doc = await JsonDocument.ParseAsync(fs);
var root = doc.RootElement;
foreach (var item in root.EnumerateArray())
{
processItem(item); // 逐条处理,不加载全部到内存
}
}
// 流式读取 JSON 数组
public async IAsyncEnumerable<T> StreamJsonArrayAsync<T>(
Stream stream, JsonSerializerOptions? options = null)
{
await using var reader = new StreamReader(stream);
await using var jsonReader = new Utf8JsonReader(reader.BaseStream);
if (jsonReader.TokenType == JsonTokenType.None)
jsonReader.Read();
if (jsonReader.TokenType != JsonTokenType.StartArray)
throw new JsonException("Expected JSON array");
while (jsonReader.Read())
{
if (jsonReader.TokenType == JsonTokenType.EndArray)
yield break;
var item = JsonSerializer.Deserialize<T>(ref jsonReader, options);
if (item != null)
yield return item;
}
}
// 使用示例:流式导入大量数据
var filePath = "large-data.json";
await using var fs = File.OpenRead(filePath);
var count = 0;
await foreach (var item in StreamJsonArrayAsync<Product>(fs))
{
// 逐条处理
await _db.Products.AddAsync(item);
count++;
if (count % 1000 == 0)
{
await _db.SaveChangesAsync(); // 分批提交
_logger.LogInformation("已导入 {Count} 条", count);
}
}
await _db.SaveChangesAsync(); // 提交剩余System.Text.Json 常见问题排查
常见问题与解决方案:
1. "A possible object cycle was detected"
→ 设置 ReferenceHandler = ReferenceHandler.IgnoreCycles
→ 或使用 DTO 避免循环引用
2. "The JSON value could not be converted to type X"
→ 检查 JSON 属性名与 C# 属性名是否匹配
→ 检查 [JsonPropertyName] 是否正确
→ 检查 JSON 数据类型与 C# 类型是否兼容
3. 中文被序列化为 Unicode 转义
→ 默认行为,JavaScriptEncoder.Default 会转义非 ASCII
→ 如需中文原文:设置 JsonSerializerOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
→ 注意:UnsafeRelaxedJsonEscaping 有 XSS 风险
4. null 值被忽略或被序列化
→ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull(忽略 null)
→ DefaultIgnoreCondition = JsonIgnoreCondition.Never(保留 null)
5. 日期格式不对
→ 默认 ISO 8601 格式("2024-01-01T00:00:00")
→ 使用自定义 JsonConverter<DateTime> 控制格式
6. 属性大小写不匹配
→ PropertyNamingPolicy = JsonNamingPolicy.CamelCase(驼峰)
→ 或使用 [JsonPropertyName("custom_name")] 精确控制
7. 序列化性能差
→ 复用 JsonSerializerOptions 实例
→ 使用 Source Generator
→ 避免 WriteIndented = true
→ 使用 DTO 减少序列化字段优点
缺点
总结
System.Text.Json 是 ASP.NET Core 默认的高性能 JSON 序列化器。关键性能配置:复用 JsonSerializerOptions、关闭 WriteIndented、使用 JsonIgnore 减少序列化字段。Source Generator 通过 [JsonSerializable] 编译时生成代码,性能提升 2x+,是 Native AOT 的必需方案。序列化库性能排名:protobuf > MessagePack > STJ Source Gen > STJ 反射 > Newtonsoft。优化策略:DTO 投影减少字段、Utf8JsonWriter 直接写入、缓存序列化结果、流式序列化大数据集合。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
- 设备接入类主题通常同时涉及协议、线程、实时刷新和异常恢复。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
- 明确采集周期、重连策略、数据缓存和状态同步方式。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
- 只验证通信成功,不验证断线、抖动和异常包。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
- 继续补齐设备模拟、离线回放、现场诊断和配置中心能力。
适用场景
- 当你准备把《序列化性能对比与调优》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《序列化性能对比与调优》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《序列化性能对比与调优》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《序列化性能对比与调优》最大的收益和代价分别是什么?
