👋 我们都想要整洁、可维护的C#代码。
但这里有个误区:整洁的代码 ≠ 可扩展的代码。
我审查过几十个通过了代码评审和单元测试的生产系统——但在真实业务流量下却崩得很惨。为什么?
因为一些看似无害的编码习惯,在系统规模化时会变成瓶颈。
如果你正在用.NET构建微服务、API或云原生应用——这篇文章能给你提个醒。
下面就来拆解15个悄悄损害可扩展性的坏习惯,以及如何修复它们。
🚫 1. 在异步操作上阻塞(.Result / .Wait())
危害:在负载下导致线程池耗尽。
var data = httpClient.GetAsync("api/user").Result; // ❌
修复方案:
var data = await httpClient.GetAsync("api/user"); // ✅
“我合作过的一个团队,仅仅通过移除热点路径循环中对Task.FromResult()的误用,就减少了47%的内存使用。”
📚 微软文档 —— 异步编程指南
🚫 2. 过度日志记录
危害:过多的日志会阻塞I/O和应用洞察(App Insights),导致延迟增加和成本飙升。
// ❌ 反模式:不做过滤,记录所有内容
builder.Logging.AddConsole();
修复方案:使用过滤器,只记录关键信息。
builder.Logging.AddFilter("Microsoft", LogLevel.Warning); // ✅
额外建议:使用语义日志而非字符串日志
// ✅ 语义日志:结构化且可查询
_logger.LogInformation("User {UserId} logged in", user.Id);
为何有效:
📚 微软文档 —— 日志过滤
语义日志
🚫 3. 加载完整实体(滥用Include())
危害:用Include()拉取完整实体会导致大量连接操作、过度抓取,增加内存使用——尤其是在不需要导航属性时。
// ❌ 反模式:加载包含导航属性的完整实体图
var orders = await db.Orders
.Include(o => o.Customer)
.Include(o => o.Items)
.ToListAsync();
修复方案:用Select()或DTOs投射所需数据。
// ✅ 微软推荐:用Select()只投射需要的内容
var orders = await db.Orders
.Select(o => new OrderDto {
Id = o.Id,
Total = o.Total
})
.ToListAsync();
📚 微软文档 —— EF Core中的查询性能注意事项
🚫 4. 不使用缓存(或误用缓存)
危害:每个用户请求都会直接访问数据库或下游API。
// ❌ 反模式:每个请求都直接调用数据库
var user = await GetUserFromDb(id);
修复方案:
// ✅ 微软推荐:使用带过期时间的内存缓存
var cached = await _cache.GetOrCreateAsync($"user:{id}", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
return await GetUserFromDb(id);
});
📚 微软文档 —— 缓存
🚫 5. 为微服务使用AddControllers()
危害:会引入整个MVC堆栈(模型绑定器、过滤器、约定和未使用的功能),增加启动时间、内存开销和复杂度。
// ❌ 反模式:为轻量级微服务使用完整MVC
builder.Services.AddControllers();
app.MapControllers();
修复方案:
// ✅ 微软推荐:为轻量级服务使用最小API
app.MapPost("/order", async (OrderDto order) =>
{
// 处理订单逻辑
});
📚 最小API
🚫 6. 读取操作忽略AsNoTracking()
危害:EF Core默认会跟踪所有查询到的实体——即便是只读操作,这会在高吞吐量场景下增加内存使用并降低性能。
// ❌ 反模式:不需要更新时仍跟踪实体
var orders = await db.Orders.ToListAsync();
修复方案:
// ✅ 微软推荐:为只读查询禁用跟踪
var orders = await db.Orders.AsNoTracking().ToListAsync();
📚 微软文档 —— EF Core中的跟踪与非跟踪查询
🚫 7. 不使用重试/熔断策略
危害:一个不稳定的数据库或下游API可能会引发微服务间的级联故障——尤其是在高负载下。
// ❌ 反模式:HttpClient没有任何弹性策略
services.AddHttpClient("MyApiClient");
修复方案(使用Polly):
// ✅ 微软推荐:添加重试和熔断策略
services.AddHttpClient("MyApiClient")
.AddPolicyHandler(HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(3, TimeSpan.FromSeconds(30)));
📚 .NET中的弹性机制
🚫 8. 在代码中硬编码配置
危害:在代码中嵌入配置值会导致更新困难,需要完整重新部署,且无法在多环境或云原生应用中实现动态配置。
// ❌ 反模式:代码中硬编码值
var retryCount = 5;
var baseUrl = "https://api.example.com";
修复方案:使用IOptionsMonitor或Azure应用配置。
// ✅ 微软推荐:使用IOptionsMonitor或外部配置源
public class MyOptions
{
public int RetryCount { get; set; }
public string BaseUrl { get; set; }
}
// 通过IOptionsMonitor<MyOptions>注入
var retryCount = _options.Value.RetryCount;
📚 微软文档 —— 选项模式
📚 Azure文档 —— Azure应用配置
🚫 9. 同步的文件或数据库操作
危害:同步I/O会阻塞线程——在高负载下,这会导致线程池耗尽、延迟增加和可扩展性瓶颈。
// ❌ 反模式:同步的文件和数据库访问
var content = File.ReadAllText("data.json");
var reader = command.ExecuteReader();
修复方案:使用ReadAsync()、ExecuteReaderAsync()等异步方法。
// ✅ 微软推荐:对I/O密集型操作使用异步API
var content = await File.ReadAllTextAsync("data.json");
var reader = await command.ExecuteReaderAsync();
📚 [微软文档 —— 文件I/O最佳实践]
📚 [微软文档 —— ADO.NET异步编程]
🚫 10. 到处使用大DTO
危害:在多个端点复用大型、万能的DTO会导致过度序列化、 payload体积增大、内存膨胀和API性能下降。
// ❌ 反模式:一个大DTO用于所有场景
public class OrderDto
{
public Guid Id { get; set; }
public CustomerDto Customer { get; set; }
public List<ItemDto> Items { get; set; }
public ShippingDetailsDto Shipping { get; set; }
public PaymentInfoDto Payment { get; set; }
public AuditMetadata Audit { get; set; }
// 即便是在轻量级列表API中也会用到
}
修复方案:为每个端点设计特定用途的DTO——拒绝“胖模型”。
// ✅ 微软推荐:为每个端点设计特定用途的DTO
public class OrderSummaryDto
{
public Guid Id { get; set; }
public decimal Total { get; set; }
}
🎯 额外建议:用record创建轻量、不可变的DTO
原因:record类型非常适合DTO这类只读数据结构。它们支持基于值的相等性、内置不可变性,并减少样板代码。
// ✅ 推荐使用C# record的DTO
public record OrderSummaryDto(Guid Id, decimal Total);
优势:
📚 [微软文档 —— Web API最佳实践]
📚 [微软文档 —— C#中的record]
🚫 11. 跳过负载测试
危害:应用在10 RPS(每秒请求数)下可能运行良好……直到遭遇生产环境的流量。没有适当的负载测试,线程耗尽、延迟峰值和故障级联往往在发现时已为时已晚。
// ❌ 反模式:生产前没有负载测试覆盖
// “在开发机上能跑”不代表性能有保障
修复方案:使用k6、Azure负载测试或Artillery等工具。
# ✅ 微软推荐:用专业工具进行负载测试
# 使用k6(开源)
k6 run loadtest.js
# 或使用Azure负载测试(全托管)
# 支持JMeter测试计划和Azure原生遥测
📚 Azure负载测试
📚 [微软学习 —— 性能测试指南]
🚫 12. 在单例中使用作用域依赖
危害:将作用域服务(如DbContext)注入单例会导致危险的对象生命周期问题——引发内存泄漏、ObjectDisposedException和不可预测的竞态条件。
// ❌ 反模式:作用域服务注入单例
builder.Services.AddSingleton<OrderProcessor>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
public class OrderProcessor
{
public OrderProcessor(IOrderRepository repo) { ... } // 💥 运行时风险!
}
修复方案:
// ✅ 微软推荐:正确匹配生命周期
builder.Services.AddScoped<OrderProcessor>(); // 或使用工厂模式控制访问
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
📚 微软文档 —— 依赖注入生命周期
🚫 13. 不对外部API调用限流
危害:无限制地调用外部API可能会压垮服务提供商,触发限流(HTTP 429),甚至导致黑名单——尤其是在流量峰值或重试时。
// ❌ 反模式:无限制的出站调用
foreach (var id in userIds)
{
await _externalApiClient.GetUserAsync(id); // 无限制、无重试、无延迟
}
修复方案:
// ✅ 微软推荐:用通道或限流器进行限流
var channel = Channel.CreateBounded<string>(new BoundedChannelOptions(10)
{
FullMode = BoundedChannelFullMode.Wait
});
var producer = Task.Run(async () =>
{
foreach (var id in userIds)
await channel.Writer.WriteAsync(id);
channel.Writer.Complete();
});
var consumer = Task.Run(async () =>
{
await foreach (var id in channel.Reader.ReadAllAsync())
{
await _externalApiClient.GetUserAsync(id);
await Task.Delay(200); // 简单限流
}
});
await Task.WhenAll(producer, consumer);
📚 微软文档 —— .NET中的通道
📚 微软文档 —— 弹性模式
🚫 14. 不为数据库建索引
危害:在开发或测试环境中看似快速的查询,在生产环境中随着表数据增长可能会大幅变慢——导致响应延迟、锁竞争和CPU峰值。
-- ❌ 反模式:大表上未索引的WHERE子句
SELECT * FROM Orders WHERE CustomerId = @CustomerId;
修复方案:使用SQL Server执行计划、索引顾问或EF Core日志进行优化。
-- ✅ 微软推荐:添加非聚集索引
CREATE NONCLUSTERED INDEX IX_Orders_CustomerId ON Orders(CustomerId);
或使用EF Core日志检测慢查询:
optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging();
📚 SQL Server索引
📚 微软文档 —— EF Core性能技巧
🚫 15. 不做任何度量
危害:没有真实指标,你就像在盲目飞行。你不会知道应用是否缓慢、内存占用过高、线程不足或存在泄漏——直到用户开始投诉。
// ❌ 反模式:发布的代码没有任何可观测性
// 没有日志、没有计数器、没有追踪
修复方案:集成dotnet-counters、Application Insights、OpenTelemetry。
# ✅ 微软推荐:用dotnet-counters进行实时诊断
dotnet-counters monitor -p YourApp
# 或用Application Insights收集遥测
builder.Services.AddApplicationInsightsTelemetry();
# 或采用OpenTelemetry进行分布式追踪
builder.Services.AddOpenTelemetry()
.WithMetrics()
.WithTracing();
📚 dotnet-counters
📚 微软文档 —— ASP.NET Core的Application Insights
📚 微软文档 —— .NET中的OpenTelemetry
🔁 结语
你可能在团队中写出了最整洁的.NET代码——
但如果它在5K RPS下就崩溃,那它就不是为生产环境构建的。
可扩展性不是事后才考虑的事,而是一种思维方式。
✅ 考虑异步。
✅ 考虑可观测性。
✅ 考虑有目的性的设计。