15个悄悄毁掉你C#代码可扩展性的坏习惯(及修复方案)

作者:微信公众号:【架构师老卢】
7-30 8:12
8

👋 我们都想要整洁、可维护的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);

为何有效:

  • 保持日志以键值对形式结构化
  • 支持在Application Insights或ELK等工具中进行过滤、查询和告警
  • 减少字符串格式化开销和日志量

📚 微软文档 —— 日志过滤

语义日志

🚫 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);

优势:

  • 序列化更简洁
  • 默认不可变(避免意外修改)
  • 自动生成Equals、GetHashCode和ToString方法

📚 [微软文档 —— 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下就崩溃,那它就不是为生产环境构建的。

可扩展性不是事后才考虑的事,而是一种思维方式。

✅ 考虑异步。
✅ 考虑可观测性。
✅ 考虑有目的性的设计。

相关留言评论
昵称:
邮箱:
阅读排行