步驟 4:使用 ADO.NET 彈性地連線到 SQL

下載 ADO.NET

本文提供一個 C# 程式碼範例,示範手動實作的重試邏輯。 重試邏輯處理暫時錯誤或 暫時錯誤,若程式等待並嘗試該操作,這些錯誤可能會被清除。

Important

對於使用 Microsoft.Data.SqlClient 3.0 及更新版本的新應用程式,建議優先使用驅動程式中可設定的重試邏輯,而不是自行實作重試迴圈。 先從 SqlClient 中的 Configure retry 邏輯開始。

暫時性錯誤的來源包括:

  • 支援網際網路的短暫故障。
  • 當你的查詢到來時,雲端系統可能正在進行負載平衡。

用於連線到 SQL Server 的 ADO.NET 類別也可以連線到 Azure SQL Database。 您的客戶端程式可能會遇到暫時性故障。 它應該會透過使用適合操作的有界重試策略來恢復。

步驟 1:識別暫時性錯誤

你的程式必須區分暫時錯誤與持續錯誤。 瞬態錯誤是指可能在短時間內消失的錯誤狀況,例如暫時性網路問題。 持續錯誤的例子是目標資料庫名稱的拼寫錯誤。 在這種情況下,「找不到此類資料庫」的錯誤會持續存在,且不會很快消失。

被歸類為暫時性錯誤的錯誤編號清單,請參閱 SQL 資料庫用戶端應用程式的錯誤訊息。

步驟 2:建立並執行範例應用程式

此範例假設已安裝 .NET Framework 4.6.2 或更新版本。 C# 程式碼範例包含一個名為 Program.cs的檔案。 下一節會提供它的程式碼。

步驟 2.a:擷取並編譯程式碼範例

您可以使用下列步驟編譯此範例:

  1. 在 免費的 Visual Studio Community 版本中,透過 C# 主控台應用程式範本建立新的專案。

    • 選擇檔案>新增>專案>已安裝>範本>Visual C#>Windows>傳統桌面>主控台應用程式。

    • 將專案命名為 RetryAdo2。

  2. 開啟 [方案總管] 窗格。

    • 查看專案的名稱。
    • 在您的專案中,在 Microsoft.Data.SqlClient 套件上新增 NuGet 相依性。
    • 查看檔案名稱 Program.cs 。
  3. 開啟 Program.cs 檔案。

  4. 將 Program.cs 檔案的全部內容替換為下方程式碼區塊中的程式碼。

  5. 選擇 「建置>建置解決方案」選單。

步驟 2.b:複製並貼上範例程式碼

把這段程式碼貼到你的 Program.cs 檔案裡。

接著,編輯伺服器名稱和密碼的字串。 你可以在名為 GetSqlConnectionString的方法中找到這些字串。

注意

這個 連接字串 是針對 Azure SQL Database 設計的,因為它包含了四個字元的前綴tcp:。 但你可以調整伺服器字串來連接到你的 SQL Server 實例。

using System;
using System.Collections.Generic;
using Microsoft.Data.SqlClient;
using System.Threading;

namespace RetryAdo2;

public class Program
{
    public static int Main(string[] args)
    {
        bool succeeded = false;
        const int totalNumberOfTimesToTry = 4;
        int retryIntervalSeconds    = 10;

        for (int tries = 1; tries <= totalNumberOfTimesToTry; tries++)
        {
            try
            {
                if (tries > 1)
                {
                    Console.WriteLine(
                        "Transient error encountered. Will begin attempt number {0} of {1} max...",
                        tries,
                        totalNumberOfTimesToTry
                    );
                    Thread.Sleep(1000 * retryIntervalSeconds);
                    retryIntervalSeconds = Convert.ToInt32(retryIntervalSeconds * 1.5);
                }
                AccessDatabase();
                succeeded = true;
                break;
            }
            catch (SqlException sqlExc) {
                if (TransientErrorNumbers.Contains(sqlExc.Number))
                {
                    Console.WriteLine("{0}: transient occurred.", sqlExc.Number);
                    continue;
                }

                Console.WriteLine(sqlExc);
                break;
            }
            catch (TestSqlException sqlExc) {
                if (TransientErrorNumbers.Contains(sqlExc.Number))
                {
                    Console.WriteLine("{0}: transient occurred. (TESTING.)", sqlExc.Number);
                    continue;
                }

                Console.WriteLine(sqlExc);
                break;
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                break;
            }
        }

        if (!succeeded) {
            Console.WriteLine("ERROR: Unable to access the database!");
            return 1;
        }

        return 0;
    }

    /// <summary>
    /// Connects to the database, reads,
    /// prints results to the console.
    /// </summary>
    static void AccessDatabase() {
        //throw new TestSqlException(4060); //(7654321);  // Uncomment for testing.

        using var sqlConnection = new SqlConnection(GetSqlConnectionString());

        using var dbCommand = sqlConnection.CreateCommand();

        dbCommand.CommandText = @"
        SELECT TOP 3
            ob.name,
            CAST(ob.object_id as NVARCHAR(32)) AS [object_id]
        FROM sys.objects AS ob
        WHERE ob.type='IT'
        ORDER BY ob.name;";
        
        sqlConnection.Open();
        var dataReader = dbCommand.ExecuteReader();

        while (dataReader.Read())
        {
            Console.WriteLine(
                "{0}\t{1}",
                dataReader.GetString(0),
                dataReader.GetString(1)
            );
        }
    }

    /// <summary>
    /// Edit the four string values in accordance with your environment.
    /// </summary>
    /// <returns>An ADO.NET connection string.</returns>
    static private string GetSqlConnectionString()
    {
        // Prepare the connection string to Azure SQL Database.
        var sqlConnectionSB = new SqlConnectionStringBuilder
        {
            // Change these values to your values.
            DataSource           = "tcp:myazuresqldbserver.database.windows.net,1433", //["Server"]
            InitialCatalog       = "MyDatabase",                                       //["Database"]
            UserID               = "MyLogin",                                          // "@yourservername"  as suffix sometimes.
            Password             = "<password>",
            // Adjust these values if you like. (ADO.NET 4.5.1 or later.)
            ConnectRetryCount    = 3,
            ConnectRetryInterval = 10, // Seconds.
            // Leave these values as they are.
            IntegratedSecurity = false,
            Encrypt            = true,
            ConnectTimeout     = 30
        };

        return sqlConnectionSB.ToString();
    }

    static List<int> TransientErrorNumbers = new()
    {
        4060, 40197, 40501, 40613, 49918, 49919, 49920, 11001
    };
}

/// <summary>
/// For testing retry logic, you can have method
/// AccessDatabase start by throwing a new
/// TestSqlException with a Number that does
/// or does not match a transient error number
/// present in TransientErrorNumbers.
/// </summary>
internal class TestSqlException : ApplicationException
{
    internal TestSqlException(int testErrorNumber)
    {
        Number = testErrorNumber;
    }

    internal int Number { get; set; }
}

步驟 2.c:執行程式

RetryAdo2.exe執行檔不會接受參數。 若要執行 .exe:

  1. 在你編譯出 RetryAdo2.exe 二進位檔的資料夾中開啟主控台視窗。
  2. 執行 RetryAdo2.exe 時不要輸入參數。
database_firewall_rules_table   245575913
filestream_tombstone_2073058421 2073058421
filetable_updates_2105058535    2105058535

步驟 3:測試重試邏輯的方式

你可以用幾種方式模擬暫態錯誤來測試重試邏輯。

步驟 3.a:拋出測試異常

程式碼範例包含:

  • 一個名為 TestSqlException 的小型第二個類別,具有一個名為 Number 的屬性。
  • //throw new TestSqlException(4060); ,您可以把它取消註解。

如果你將 throw 陳述取消註解並重新編譯,下次執行 RetryAdo2.exe 時,輸出會類似如下。

[C:\VS15\RetryAdo2\RetryAdo2\bin\Debug\]
>> RetryAdo2.exe
4060: transient occurred. (TESTING.)
Transient error encountered. Will begin attempt number 2 of 4 max...
4060: transient occurred. (TESTING.)
Transient error encountered. Will begin attempt number 3 of 4 max...
4060: transient occurred. (TESTING.)
Transient error encountered. Will begin attempt number 4 of 4 max...
4060: transient occurred. (TESTING.)
ERROR: Unable to access the database!

[C:\VS15\RetryAdo2\RetryAdo2\bin\Debug\]
>>

步驟 3.b:使用持續存在的錯誤重新測試

為了證明程式碼能正確處理持續錯誤,請重跑前一個測試。 與其使用真實的暫態錯誤號碼如4060,不如使用虛構的數字7654321。 程式將此數字視為持續錯誤,並繞過任何重試。

步驟 3.c:中斷網路連線

  1. 中斷用戶端電腦的網路連線。
    • 若為桌上型電腦,請拔除網路線。
    • 若為膝上型電腦,請按功能鍵組合來關閉網路介面卡。
  2. 開始 RetryAdo2.exe,等待主控台顯示第一個暫態錯誤,可能是 11001。
  3. 重新連接網路,同時 RetryAdo2.exe 繼續運行。
  4. 觀看主控台報告後續的重試成功。

步驟 3.d:暫時拼錯伺服器名稱

  1. 暫時將 40615 作為另一個錯誤編號加入至 TransientErrorNumbers 中,然後重新編譯。
  2. 在 new QC.SqlConnectionStringBuilder()這行設定中斷點。
  3. 使用「編輯後繼續」 功能,在以下幾行中刻意拼錯伺服器名稱。
    • 讓程式執行,並返回您的中斷點。
    • 會發生錯誤 40615。
  4. 修正拼字錯誤。
  5. 讓程式執行並成功完成。
  6. 移除 40615,然後重新編譯。

後續步驟