Skip to content

连接池问题 ​

概述 ​

数据库连接池(Connection Pooling)是 ADO.NET 的核心机制,用于复用数据库连接以提升性能。然而,配置不当或资源泄漏会导致连接池耗尽、超时等问题,严重影响应用稳定性。

连接池工作原理 ​

应用程序请求连接
    ↓
检查池中是否有可用连接?
    ├─ 有 → 返回现有连接(快速)
    └─ 无 → 创建新连接(较慢)
         ↓
    池是否已满?
         ├─ 未满 → 创建新连接并加入池中
         └─ 已满 → 等待直到超时(Connection Timeout)

常见问题与解决方案 ​

❌ 问题 1: 连接池耗尽 ​

症状:

Timeout expired. The timeout period elapsed prior to obtaining 
a connection from the pool. This may have occurred because all 
pooled connections were in use and max pool size was reached.

原因分析:

  1. 连接未正确关闭: 忘记调用 Close() 或 Dispose()
  2. 长事务: 持有连接时间过长
  3. 并发过高: 超过最大池大小
  4. 连接泄漏: 异常时未释放连接

解决方案:

方案 1: 确保连接正确释放 ​

csharp
// ❌ 错误: 连接可能不会被关闭
public async Task<List<Product>> GetProducts()
{
    var connection = new SqlConnection(connectionString);
    var products = await connection.QueryAsync<Product>("SELECT * FROM Products");
    // 如果 QueryAsync 抛出异常,连接不会关闭!
    connection.Close();
    return products.ToList();
}

// ✅ 正确: 使用 using 语句
public async Task<List<Product>> GetProducts()
{
    await using var connection = new SqlConnection(connectionString);
    return (await connection.QueryAsync<Product>("SELECT * FROM Products")).ToList();
}

// ✅ 更好: 依赖注入自动管理
public class ProductService
{
    private readonly AppDbContext _context;
    
    public ProductService(AppDbContext context)
    {
        _context = context; // DbContext 自动管理连接生命周期
    }
}

方案 2: 调整连接池大小 ​

csharp
// 默认连接池大小为 100
// 根据实际并发需求调整
var connectionString = "Server=myServer;Database=myDB;Trusted_Connection=True;" +
                      "Max Pool Size=200;" +      // 最大连接数
                      "Min Pool Size=5;" +         // 最小连接数
                      "Connection Lifetime=300;" + // 连接最大生存期(秒)
                      "Connection Timeout=30";     // 获取连接超时时间(秒)

builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connectionString);
});

参数说明:

  • Max Pool Size: 默认 100,高并发场景可调至 200-500
  • Min Pool Size: 默认 0,预创建连接减少首次访问延迟
  • Connection Lifetime: 防止连接永久占用,建议设置为 300-600 秒
  • Connection Timeout: 获取连接超时,默认 15 秒

❌ 问题 2: 连接泄漏 ​

症状:

  • 连接池逐渐耗尽
  • 应用运行一段时间后变慢
  • 重启后恢复正常

检测方法:

csharp
// 监控连接池状态
public class ConnectionPoolMonitor : BackgroundService
{
    private readonly ILogger<ConnectionPoolMonitor> _logger;
    private readonly string _connectionString;
    
    public ConnectionPoolMonitor(
        ILogger<ConnectionPoolMonitor> logger,
        IConfiguration config)
    {
        _logger = logger;
        _connectionString = config.GetConnectionString("Default");
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                // 获取连接池统计信息
                var poolCount = SqlConnection.ClearAllPools();
                
                // 使用性能计数器监控
                var counters = new PerformanceCounterCategory(".NET Data Provider for SqlServer");
                var instances = counters.GetInstanceNames();
                
                foreach (var instance in instances)
                {
                    var numberOfActiveConnections = new PerformanceCounter(
                        ".NET Data Provider for SqlServer",
                        "NumberOfActiveConnections",
                        instance);
                    
                    var numberOfFreeConnections = new PerformanceCounter(
                        ".NET Data Provider for SqlServer",
                        "NumberOfFreeConnections",
                        instance);
                    
                    _logger.LogInformation(
                        $"Instance: {instance}, Active: {numberOfActiveConnections.NextValue()}, " +
                        $"Free: {numberOfFreeConnections.NextValue()}");
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error monitoring connection pool");
            }
            
            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
        }
    }
}

// 注册后台服务
builder.Services.AddHostedService<ConnectionPoolMonitor>();

修复连接泄漏:

csharp
// ❌ 错误: 异常时连接不会释放
public async Task UpdateProduct(int id, decimal price)
{
    await using var connection = new SqlConnection(connectionString);
    await connection.ExecuteAsync(
        "UPDATE Products SET Price = @Price WHERE Id = @Id",
        new { Price = price, Id = id });
    // 如果 ExecuteAsync 抛出异常,using 会正确处理
}

// ✅ 正确: 添加重试和熔断机制
public async Task UpdateProductWithRetry(int id, decimal price)
{
    var retryPolicy = Policy
        .Handle<SqlException>()
        .WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(attempt));
    
    await retryPolicy.ExecuteAsync(async () =>
    {
        await using var connection = new SqlConnection(connectionString);
        await connection.OpenAsync();
        await connection.ExecuteAsync(
            "UPDATE Products SET Price = @Price WHERE Id = @Id",
            new { Price = price, Id = id });
    });
}

❌ 问题 3: 连接池碎片化 ​

原因: 使用不同的连接字符串导致创建多个连接池

csharp
// ❌ 错误: 每次创建新的连接池
public void BadExample(string userId)
{
    // 每个 userId 都会创建新的连接池!
    var connString = $"Server=myServer;Database=myDB;User ID={userId};";
    using var connection = new SqlConnection(connString);
}

// ✅ 正确: 复用相同的连接字符串
private static readonly string SharedConnectionString = 
    ConfigurationManager.ConnectionStrings["Default"].ConnectionString;

public void GoodExample()
{
    using var connection = new SqlConnection(SharedConnectionString);
}

检测碎片化:

csharp
// 查看当前活动的连接池数量
var pools = SqlConnection.ClearAllPools();
Console.WriteLine($"Total pools cleared: {pools}");

// 理想情况: 应该只有 1-2 个连接池
// 如果有几十个,说明存在碎片化问题

❌ 问题 4: 长时间运行的事务 ​

问题: 事务持有连接时间过长,阻塞其他请求

csharp
// ❌ 错误: 事务中包含耗时操作
public async Task ProcessOrder(int orderId)
{
    await using var connection = new SqlConnection(connectionString);
    await connection.OpenAsync();
    
    await using var transaction = await connection.BeginTransactionAsync();
    
    try
    {
        // 数据库操作
        await connection.ExecuteAsync(
            "UPDATE Orders SET Status = 'Processing' WHERE Id = @Id",
            new { Id = orderId }, transaction);
        
        // ⚠️ 调用外部 API(持有连接!)
        await _paymentGateway.ChargeAsync(orderId);
        
        // ⚠️ 发送电子邮件(持有连接!)
        await _emailService.SendConfirmationAsync(orderId);
        
        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
}

// ✅ 正确: 缩短事务范围
public async Task ProcessOrderOptimized(int orderId)
{
    Order order;
    
    // 步骤 1: 先执行数据库查询(短事务)
    await using (var connection = new SqlConnection(connectionString))
    {
        await connection.OpenAsync();
        order = await connection.QueryFirstOrDefaultAsync<Order>(
            "SELECT * FROM Orders WHERE Id = @Id", new { Id = orderId });
    } // 连接立即释放
    
    // 步骤 2: 执行外部调用(不持有数据库连接)
    await _paymentGateway.ChargeAsync(orderId);
    await _emailService.SendConfirmationAsync(orderId);
    
    // 步骤 3: 更新数据库(短事务)
    await using (var connection = new SqlConnection(connectionString))
    {
        await connection.OpenAsync();
        await connection.ExecuteAsync(
            "UPDATE Orders SET Status = 'Completed' WHERE Id = @Id",
            new { Id = orderId });
    } // 连接立即释放
}

最佳实践 ​

✅ 1. 使用 DbContext(推荐) ​

csharp
// DbContext 自动管理连接池,无需手动处理
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connectionString);
});

public class ProductService
{
    private readonly AppDbContext _context;
    
    public ProductService(AppDbContext context)
    {
        _context = context; // 自动从池中获取和释放连接
    }
    
    public async Task<List<Product>> GetProductsAsync()
    {
        return await _context.Products.ToListAsync();
        // 查询完成后,连接自动返回池中
    }
}

✅ 2. 合理设置超时时间 ​

csharp
var connectionString = "Server=myServer;" +
                      "Connection Timeout=30;" +    // 获取连接超时
                      "Command Timeout=60;";         // 命令执行超时

// 或在代码中设置
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connectionString, sqlOptions =>
    {
        sqlOptions.CommandTimeout(60); // 秒
    });
});

✅ 3. 监控连接池健康度 ​

csharp
// 添加健康检查
builder.Services.AddHealthChecks()
    .AddSqlServer(
        connectionString,
        healthQuery: "SELECT 1",
        name: "sqlserver",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "db", "sql" });

// 健康检查端点
app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = async (context, report) =>
    {
        context.Response.ContentType = "application/json";
        await context.Response.WriteAsJsonAsync(new
        {
            status = report.Status.ToString(),
            checks = report.Entries.Select(e => new
            {
                name = e.Key,
                status = e.Value.Status.ToString(),
                duration = e.Value.Duration.TotalMilliseconds,
                description = e.Value.Description
            })
        });
    }
});

✅ 4. 清理连接池 ​

csharp
// 紧急情况下清空连接池
public class EmergencyController : ControllerBase
{
    [HttpPost("admin/clear-connection-pool")]
    public IActionResult ClearConnectionPool()
    {
        // 清除所有连接池
        SqlConnection.ClearAllPools();
        
        return Ok(new { message = "All connection pools cleared" });
    }
    
    [HttpPost("admin/clear-specific-pool")]
    public IActionResult ClearSpecificPool([FromBody] string connectionString)
    {
        // 清除特定连接池
        SqlConnection.ClearPool(new SqlConnection(connectionString));
        
        return Ok(new { message = "Specific pool cleared" });
    }
}

性能调优建议 ​

连接池大小计算 ​

Max Pool Size = (并发请求数 × 平均请求处理时间) / 请求间隔时间

示例:
- 并发请求数: 100 req/s
- 平均处理时间: 0.1s
- 请求间隔: 1s

Max Pool Size = 100 × 0.1 / 1 = 10

建议预留 20-50% 余量: 10 × 1.5 = 15

推荐配置 ​

场景Min Pool SizeMax Pool SizeConnection LifetimeTimeout
开发环境0500 (无限)30s
小型应用5100300s30s
中型应用10200300s30s
大型应用20500600s60s
高并发501000600s60s

故障排查清单 ​

🔍 检查项 ​

  • [ ] 是否使用 using 语句确保连接释放?
  • [ ] 连接字符串是否保持一致(避免碎片化)?
  • [ ] 最大池大小是否足够(监控峰值使用)?
  • [ ] 是否存在长事务持有连接?
  • [ ] 是否正确处理异常时的连接释放?
  • [ ] 是否启用了连接池监控?
  • [ ] 连接超时设置是否合理?
  • [ ] 是否有连接泄漏(对比打开/关闭次数)?

总结 ​

核心原则 ​

  1. 让 EF Core 管理连接: 使用 DbContext,不要手动管理
  2. 快速借还: 缩短连接持有时间
  3. 监控预警: 实时监控连接池健康度
  4. 合理配置: 根据实际负载调整参数
  5. 防御编程: 始终假设会出现异常

常见错误 TOP 3 ​

  1. ❌ 忘记释放连接(未使用 using)
  2. ❌ 在事务中执行耗时操作
  3. ❌ 使用不同的连接字符串导致碎片化

遵循这些最佳实践,可避免 95% 以上的连接池问题!

基于 MIT 许可发布