如何检测 SqlConnection 的"blocking period"是否处于活动状态?



根据微软的文章(SQL Server连接池(ADO.NET)),

启用连接池后,如果发生超时错误或其他登录错误,将引发异常,随后的连接尝试将在接下来的五秒钟内失败,即"阻塞期"。如果应用程序试图在阻塞期内连接,则会再次引发第一个异常。阻塞期结束后,应用程序的另一次连接失败将导致阻塞期是前一个阻塞期的两倍。阻塞期结束后的后续故障将导致新的阻塞期,该阻塞期是前一个阻塞期的两倍,最长可达五分钟。

您将如何检测到阻塞期处于活动状态?我假设在尝试连接之前要检查一些属性,这样就可以避免延长阻塞期。

不需要检查您是否处于阻塞期以避免延长阻塞期。正如上面摘录的那样,在阻塞期内任何连接尝试都会重新抛出第一个异常,这与延长阻塞期无关。但是,每个新的阻塞周期将是前一个的两倍长。

根据我的经验,抛出的异常(由于超时、连接泄漏等)要么是环境问题,要么是未能正确关闭/处理连接。记录这些异常是个好主意,这样您就可以追踪到真正的问题。

如果你确实不断遇到超时异常,你可以抓住它并尝试清除所有池,但这很可能是由于连接泄漏。您需要确保使用using语句包装连接,这将有助于在处理完连接或发生异常时关闭/处理连接。

using(SqlConnection connection = new SqlConnection("connection_string"))
{
    using(SqlCommand command = new SqlCommand())
    {
        SqlCommand command = new SqlCommand();
        command.Connection = connection;
        command.CommandType = CommandType.Text;
        command.CommandTimeout = [some timeout value]; 
        command.CommandText = "Update SomeTable Set Value = 1";
        connection.Open();
        command.ExecuteNonQuery();
    }
}

除了ClientConnectionId字段外,

SqlException.Message也将是reference equal。也就是说,对于在"阻塞期"内失败的连接,将返回缓存的字符串。

然而,这也是一个实现细节,可能会发生变化。

不幸的是,没有简单的方法可以检测您是否处于ADO.NET"阻塞期"(而不需要求助于像反射这样脆弱的东西)。

但是,如果您使用.Net 4.5或更高版本,则可以通过查看SqlExceptionClientConnectionId并将其与您看到的最后一个SqlException的id进行比较来检测您从Open/OpenAsync观察到的最后一次异常是否重复(由于异常重复,因此id也重复)。

假设您有一个单独的位置为单个连接字符串创建\open SqlConnections,则可以执行以下操作:

public static class DataAccessLayer
{
    // Single connection string that all connections use
    private static readonly string _connectionString = "server=(local);integrated security=true;";
    // Stores that last observed connection if when opening a connection
    // NOTE: Using an object so that the Volatile methods will work
    private static object _lastErrorConnectionId = Guid.Empty;
    public static SqlConnection GetOpenedConnection()
    {
        try
        {
            SqlConnection connection = new SqlConnection(_connectionString);
            connection.Open();
            return connection;
        }
        catch (SqlException ex)
        {
            // Did the connection open get to the point of creating an internal connection?
            if (ex.ClientConnectionId != Guid.Empty)
            {
                // Verify that the connection id is something new
                var lastId = (Guid)Volatile.Read(ref _lastErrorConnectionId);
                if (ex.ClientConnectionId != lastId)
                {
                    // New error, save id and fall-through to re-throw
                    // NOTE: There is a small timing window here where multiple threads could end up switching this between
                    //       a duplicated id and a new id. Since this is unlikely and will only cause a few additional exceptions to be
                    //       thrownlogged, there isn't a large need for a lock here.
                    Volatile.Write(ref _lastErrorConnectionId, (object)ex.ClientConnectionId);
                }
                else
                {
                    // Duplicate error
                    throw new DuplicatedConnectionOpenException(_connectionString, ex);
                }
            }
            // If we are here, then this is a new exception
            throw;
        }
    }
}
public class DuplicatedConnectionOpenException : Exception
{
    public string ConnectionString { get; private set; }
    internal DuplicatedConnectionOpenException(string connectionString, SqlException innerException)
        : base("Hit the connection pool block-out period and a duplicated SqlException was thrown", innerException)
    {
        ConnectionString = connectionString;
    }
}

现在,如果你调用GetOpenedConnection,看到一个DuplicatedConnectionOpenException被抛出,你就会知道你已经达到了"阻塞期"。

注意:我在这里使用Volatile Read/Write而不是锁,因为我选择了更好的性能,而不是100%准确地处于"阻塞期"。如果您更喜欢精度,可以使用lock

此外,我确实有一些代码可以作为SqlConnection上的扩展方法,可以处理多个连接字符串,但它的性能要差得多,因为它使用ConcurrentDictionary将连接字符串映射到连接ID。

相关内容

最新更新