1. 当数据库变成"蜗牛":性能问题的典型场景

(场景:电商平台订单查询页面响应时间从200ms增长到3秒)

某天早晨,你的咖啡还没喝完,运维团队就发来警报:订单系统的MongoDB集群CPU使用率持续超过90%。用户开始抱怨搜索历史订单需要等待"转三个圈圈"的时间。此时你发现,随着订单量突破千万级,原本流畅的查询操作突然变成了性能瓶颈。

这种场景的常见诱因包括:

  • 未合理使用索引的全集合扫描
  • 频繁的小批量写入操作
  • 超大文档的无节制返回
  • 连接池配置不当导致的资源浪费

2. 利刃出鞘:索引优化实战

(技术栈:MongoDB.Driver 2.19 + C# 10)

2.1 创建复合索引的正确姿势

var ordersCollection = database.GetCollection<Order>("orders");

// 创建包含常用查询字段的复合索引
var indexKeys = Builders<Order>.IndexKeys
    .Ascending(o => o.UserId)
    .Ascending(o => o.OrderDate)
    .Descending(o => o.TotalAmount);

var indexOptions = new CreateIndexOptions { 
    Name = "UserId_OrderDate_TotalAmount", 
    Background = true // 后台构建避免阻塞
};

await ordersCollection.Indexes.CreateOneAsync(
    new CreateIndexModel<Order>(indexKeys, indexOptions));

/* 索引使用场景:
   1. 按用户ID查询最近订单
   2. 按金额范围过滤用户订单
   3. 排序时避免内存排序 */

2.2 索引使用情况分析

var explainResult = await ordersCollection
    .Find(o => o.UserId == "user123")
    .SortByDescending(o => o.OrderDate)
    .ExplainAsync();

var executionStats = explainResult.ToBsonDocument()
    ["executionStats"].AsBsonDocument;

Console.WriteLine($"索引名称: {executionStats["indexName"]}"); 
Console.WriteLine($"文档扫描数: {executionStats["totalDocsExamined"]}");

3. 批量操作的艺术:告别单点爆破

(示例:批量更新用户状态)

3.1 批量写入优化

var usersCollection = database.GetCollection<User>("users");

// 低效写法:循环单次更新
foreach (var userId in expiredUserIds)
{
    await usersCollection.UpdateOneAsync(
        u => u.Id == userId,
        Builders<User>.Update.Set(u => u.IsActive, false));
}

// 高效写法:批量写入
var updates = expiredUserIds.Select(userId => 
    new UpdateOneModel<User>(
        Builders<User>.Filter.Eq(u => u.Id, userId),
        Builders<User>.Update.Set(u => u.IsActive, false))
    {
        IsUpsert = false
    });

var bulkResult = await usersCollection.BulkWriteAsync(updates, 
    new BulkWriteOptions { IsOrdered = false }); // 并行执行

Console.WriteLine($"影响文档数: {bulkResult.ModifiedCount}");

4. 查询优化三板斧:精准打击性能黑洞

4.1 投影优化示例

// 错误示范:获取整个文档
var fullDocument = await ordersCollection
    .Find(o => o.OrderId == "ORDER123")
    .FirstOrDefaultAsync();

// 正确姿势:按需获取字段
var projection = Builders<Order>.Projection
    .Include(o => o.OrderDate)
    .Include(o => o.Status)
    .Include(o => o.Items.Count);

var partialData = await ordersCollection
    .Find(o => o.OrderId == "ORDER123")
    .Project<Order>(projection)
    .FirstOrDefaultAsync();

4.2 聚合管道优化

var aggregation = await ordersCollection.Aggregate()
    .Match(o => o.OrderDate >= DateTime.UtcNow.AddMonths(-3))
    .Group(o => o.UserId, 
        g => new {
            UserId = g.Key,
            TotalSpent = g.Sum(o => o.TotalAmount),
            OrderCount = g.Count()
        })
    .SortByDescending(r => r.TotalSpent)
    .Limit(100)
    .ToListAsync();

/* 管道优化要点:
   1. $match阶段尽量前置
   2. 使用$project减少中间文档尺寸
   3. 利用allowDiskUse处理大数据集 */

5. 连接池:容易被忽视的性能杀手

(配置示例)

var settings = MongoClientSettings.FromUrl(
    new MongoUrl("mongodb://localhost:27017"));

settings.MaxConnectionPoolSize = 100; // 默认100
settings.MinConnectionPoolSize = 10;  // 默认0
settings.ConnectTimeout = TimeSpan.FromSeconds(5);
settings.SocketTimeout = TimeSpan.FromSeconds(30);

var client = new MongoClient(settings);

6. 高级技巧:分片策略与读写分离

6.1 分片配置示例

var adminDatabase = client.GetDatabase("admin");

// 启用分片功能
await adminDatabase.RunCommandAsync(
    new BsonDocumentCommand<BsonDocument>(new BsonDocument {
        { "enableSharding", "orderDB" }
    }));

// 创建分片键索引
var shardKey = Builders<Order>.IndexKeys
    .Ascending(o => o.Region)
    .Ascending(o => o.OrderDate);

await ordersCollection.Indexes.CreateOneAsync(
    new CreateIndexModel<Order>(shardKey, 
    new CreateIndexOptions { 
        Name = "ShardKey_Region_OrderDate" 
    }));

// 设置分片规则
await adminDatabase.RunCommandAsync(
    new BsonDocumentCommand<BsonDocument>(new BsonDocument {
        { "shardCollection", "orderDB.orders" },
        { "key", new BsonDocument {
            { "Region", 1 },
            { "OrderDate", 1 }
        }}
    }));

7. 技术选型的双刃剑:MongoDB优化优缺点分析

优势场景

  • 灵活应对数据结构变化
  • 水平扩展能力突出
  • 聚合分析性能优异

潜在风险

  • 事务性能成本较高
  • 内存消耗需要精细控制
  • 索引维护成本随数据量增长

8. 避坑指南:必须记住的注意事项

  1. 定期运行db.currentOp()监控慢查询
  2. 索引数量控制在每个集合5-8个以内
  3. 使用$explain分析执行计划
  4. 避免在文档中存储超大数组
  5. 写关注级别根据业务需求调整

9. 实战经验总结

通过优化某物流平台的订单系统,我们实现了:

  • 查询响应时间从3.2秒降至180ms
  • 写入吞吐量提升4倍
  • 服务器资源消耗降低60%

关键收获:

  • 索引不是越多越好,要定期清理
  • 批量操作是性能提升的银弹
  • 监控工具比直觉更可靠