閒置連線復原能力

下載 PHP 驅動程式

當伺服器與工作階段狀態支援復原時,連線韌性可讓 Microsoft ODBC Driver for SQL Server 復原已中斷的閒置連線。 PHP 擴充套件會將 ConnectRetryCount 和 ConnectRetryInterval 傳遞給 ODBC;它們沒有實作重新連線協定。 ODBC 文件列出了對 SQL Server 2014 及以後版本、Azure SQL Database 以及 Microsoft Fabric 中 SQL 資料庫的支援。 不要從 Fabric 中對 SQL 資料庫的支援推斷出對 Warehouse 的支援。

關鍵字 值 預設值 描述
ConnectRetryCount 介於 0 和 255 (含) 之間的整數 1 重新建立已中斷連線的嘗試次數上限,超過即放棄。 值為 0 時,會停用重新連線。
ConnectRetryInterval 介於 1 和 60 (含) 之間的整數 10 嘗試間隔的等待秒數。 如果 ConnectRetryCount 是 0,則忽略。

該語句的查詢逾時限制了 ODBC 在執行時恢復閒置連線的時間。 在 SQLSRV 語句選項中設定 QueryTimeout ,或 PDO::SQLSRV_ATTR_QUERY_TIMEOUT 用於PDO_SQLSRV。 LoginTimeout 控制建立連結;增加該值不會延長陳述式的復原預算。

在 SQLSRV 連線選項陣列中設定相同的重試關鍵字,例如 ["ConnectRetryCount" => 2, "ConnectRetryInterval" => 5],或在 PDO_SQLSRV 資料來源名稱(DSN)中設定相同的重試關鍵字,例如 ConnectRetryCount=2;ConnectRetryInterval=5。 這些設定不會重播失敗的交易,也不會重試應用程式的初始連線呼叫。

關於查詢執行時發生的暫態錯誤的應用程式層重試(閒置連線韌性不涵蓋此部分),請參考 Microsoft Drivers for PHP for SQL Server中的重試模式。

選擇復原原則

閒置連線恢復會保留可復原的工作階段,使應用程式能繼續使用其 PHP 連線控制代碼。 它不是一個關於再次執行命令的一般指示。

Failure 閒置連線韌性(Idle Connection Resiliency)能處理嗎? 應用動作
無法建立初始連線 No. 沒有既定的會話可以恢復。 檢查連線錯誤。 只以有界策略重試暫時性失敗。
閒置且可恢復的連線中斷 當連線再次被使用時,ODBC 可以嘗試恢復。 預留足夠的 statement timeout 時間進行預期的復原嘗試;但仍能處理最終失敗。
陳述式執行失敗 不要假設自己能恢復或重播。 檢查錯誤並判斷操作是否完成。 如果申請中可以接受重複,請再試一次只讀。
連線在交易過程中中斷 不要以為交易可以繼續。 放棄中斷的會話,確認交易是否已提交,再決定是否再嘗試。
用戶端明確關閉其 PHP 連線 No. 關閉 handle 是應用程式清理,不是閒置斷線。 如果還需要更多工作,就重新開啟新的連線。

保持恢復間隔和查詢截止日期一致。 例如,程式會使用兩次重連嘗試,間隔為 5 秒,語句逾時為 30 秒。 連線嘗試也需要時間;將次數乘以間隔時間並不保證總恢復時間。 最後一次逾時仍需錯誤處理。

備註

連線彈性僅在連線閒置時適用。 交易期間發生失敗不會觸發重新連線。 以下會話狀態也阻止重新連線:

  • 臨時資料表。
  • 全域與本地游標。
  • 交易上下文與會話層級交易鎖。
  • 應用程式鎖
  • EXECUTE AS / REVERT 安全脈絡。
  • 物件連結與嵌入(OLE)自動化控制代碼。
  • 準備可擴充標記語言(XML)的句柄。
  • 追蹤旗標。

範例

每個程式會開啟兩個連接到測試 SQL Server 實例的連線。 第二條連線以 KILL 終止第一個會話。 隨後對第一個連線的查詢會觸發 ODBC 的閒置連線恢復功能。 查詢使用內嵌列,因此不需要設定表格。 這些程式會在斷開連線前釋放每個語句,並驗證恢復查詢的值及變更的伺服器登入時間。 光有 session ID 並不代表有復原,因為 SQL Server 可以重複使用它。

Caution

僅在測試執行個體上執行此範例。 登入帳戶需要有使用 KILL 終止工作階段的權限。 程式碼只會終止剛開啟的那個會話。 不要替換其他應用程式的會話 ID。

在程序環境中設定 SQL_SERVER、SQL_DATABASE、SQL_USER 和 SQL_PASSWORD。 設定名稱與 SQL_SERVER 相符的受信任伺服器憑證。 此範例使用 SQL 驗證連線到測試 SQL Server 執行個體。 這不是 Fabric 驗證或受控識別驗證的範例。

將實際的資料庫名稱、使用者名稱和密碼存入環境變數中。 每個程式在 PHP 驅動程式的 ODBC 式連線處理中,透過加倍閉合括號並將每個值包圍在大括號中,從而將這些值跳脫一次。 兩個連線都會重複使用逸出值。

SQLSRV

將程式儲存為 recover-sqlsrv.php,並執行 php recover-sqlsrv.php。 助手在 SQLSRV 失敗時會拋出例外;它不會重試任何應用程式陳述句。

<?php
function requireSqlsrv($result)
{
    if ($result === false) {
        throw new RuntimeException(print_r(sqlsrv_errors(), true));
    }
    return $result;
}

$config = [];
foreach (['SQL_SERVER', 'SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
    $value = getenv($name);
    if ($value === false || $value === '') {
        throw new RuntimeException("Set $name before running this example.");
    }
    $config[$name] = $value;
}
foreach (['SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
    $config[$name] = '{' . str_replace('}', '}}', $config[$name]) . '}';
}
$connectionInfo = [
    'Database' => $config['SQL_DATABASE'],
    'UID' => $config['SQL_USER'],
    'PWD' => $config['SQL_PASSWORD'],
    'Encrypt' => true,
    'TrustServerCertificate' => false,
    'LoginTimeout' => 30,
    'ConnectRetryCount' => 2,
    'ConnectRetryInterval' => 5,
];
$conn = requireSqlsrv(sqlsrv_connect($config['SQL_SERVER'], $connectionInfo));
$breaker = null;
try {
    $breaker = requireSqlsrv(sqlsrv_connect($config['SQL_SERVER'], $connectionInfo));
    $sessionQuery = "SELECT @@SPID AS SessionId, "
        . "CONVERT(varchar(33), login_time, 126) AS LoginTime "
        . "FROM sys.dm_exec_sessions WHERE session_id = @@SPID";
    $idStatement = requireSqlsrv(sqlsrv_query($conn, $sessionQuery));
    $before = requireSqlsrv(sqlsrv_fetch_array($idStatement, SQLSRV_FETCH_ASSOC));
    sqlsrv_free_stmt($idStatement);
    if (!is_array($before) || !is_int($before['SessionId']) || $before['SessionId'] <= 0) {
        throw new RuntimeException('Expected the current session identity.');
    }
    $spid = $before['SessionId'];

    $query = 'SELECT id FROM (VALUES (1), (2), (3)) AS Demo(id) ORDER BY id';
    $options = ['Scrollable' => SQLSRV_CURSOR_CLIENT_BUFFERED, 'QueryTimeout' => 30];
    $stmt = requireSqlsrv(sqlsrv_query($conn, $query, [], $options));
    if (requireSqlsrv(sqlsrv_num_rows($stmt)) !== 3) {
        throw new RuntimeException('Expected three rows before the disconnect.');
    }
    sqlsrv_free_stmt($stmt);

    // KILL doesn't accept a parameter marker; $spid is a checked server-returned integer.
    $kill = requireSqlsrv(sqlsrv_query($breaker, 'KILL ' . $spid));
    sqlsrv_free_stmt($kill);
    // Allow the simulated disconnect to reach the client before the next query.
    sleep(1);

    $stmt = requireSqlsrv(sqlsrv_query($conn, $query, [], $options));
    $ids = [];
    while (($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) !== null) {
        $row = requireSqlsrv($row);
        $ids[] = $row['id'];
    }
    if ($ids !== [1, 2, 3]) {
        throw new RuntimeException('Unexpected recovered rows or PHP types.');
    }
    sqlsrv_free_stmt($stmt);

    $idStatement = requireSqlsrv(sqlsrv_query($conn, $sessionQuery));
    $after = requireSqlsrv(sqlsrv_fetch_array($idStatement, SQLSRV_FETCH_ASSOC));
    sqlsrv_free_stmt($idStatement);
    if (!is_array($after) || $after['LoginTime'] === $before['LoginTime']) {
        throw new RuntimeException('Expected a new physical session.');
    }
    echo "Recovered connection: 3 matching rows; login time changed.\n";
} finally {
    if (is_resource($breaker)) {
        sqlsrv_close($breaker);
    }
    sqlsrv_close($conn);
}

PDO_SQLSRV

將這個獨立程式存為 recover-pdo.php,並以相同的環境和權限執行 php recover-pdo.php 。 它明確設定數值擷取,使整數斷言與 SQLSRV 程式相符。 PDO 沒有 sqlsrv_errors() 呼叫功能;在例外模式下,操作失敗會擲出 PDOException。

<?php
$config = [];
foreach (['SQL_SERVER', 'SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
    $value = getenv($name);
    if ($value === false || $value === '') {
        throw new RuntimeException("Set $name before running this example.");
    }
    $config[$name] = $value;
}
foreach (['SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
    $config[$name] = '{' . str_replace('}', '}}', $config[$name]) . '}';
}
$server = $config['SQL_SERVER'];
$database = $config['SQL_DATABASE'];
$dsn = "sqlsrv:Server=$server;Database=$database;Encrypt=true;"
    . "TrustServerCertificate=false;LoginTimeout=30;"
    . "ConnectRetryCount=2;ConnectRetryInterval=5";
$attributes = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE => true,
    PDO::SQLSRV_ATTR_QUERY_TIMEOUT => 30,
];
$pdo = new PDO($dsn, $config['SQL_USER'], $config['SQL_PASSWORD'], $attributes);
$breaker = null;
$stmt = null;
try {
    $breaker = new PDO($dsn, $config['SQL_USER'], $config['SQL_PASSWORD'], $attributes);
    $sessionQuery = "SELECT @@SPID AS SessionId, "
        . "CONVERT(varchar(33), login_time, 126) AS LoginTime "
        . "FROM sys.dm_exec_sessions WHERE session_id = @@SPID";
    $stmt = $pdo->query($sessionQuery);
    $before = $stmt->fetch(PDO::FETCH_ASSOC);
    $stmt = null;
    if (!is_array($before) || !is_int($before['SessionId']) || $before['SessionId'] <= 0) {
        throw new RuntimeException('Expected the current session identity.');
    }
    $spid = $before['SessionId'];
    $query = 'SELECT id FROM (VALUES (1), (2), (3)) AS Demo(id) ORDER BY id';
    $stmt = $pdo->query($query);
    if ($stmt->fetchAll(PDO::FETCH_COLUMN) !== [1, 2, 3]) {
        throw new RuntimeException('Unexpected initial rows or PHP types.');
    }
    $stmt = null;

    // KILL requires a checked integer, not a parameter marker.
    $stmt = $breaker->query('KILL ' . $spid);
    $stmt = null;
    sleep(1);

    $stmt = $pdo->query($query);
    if ($stmt->fetchAll(PDO::FETCH_COLUMN) !== [1, 2, 3]) {
        throw new RuntimeException('Unexpected recovered rows or PHP types.');
    }
    $stmt = null;
    $stmt = $pdo->query($sessionQuery);
    $after = $stmt->fetch(PDO::FETCH_ASSOC);
    if (!is_array($after) || $after['LoginTime'] === $before['LoginTime']) {
        throw new RuntimeException('Expected a new physical session.');
    }
    echo "Recovered connection: 3 matching rows; login time changed.\n";
} finally {
    $stmt = null;
    $breaker = null;
    $pdo = null;
}

當 ODBC 恢復連線時,任一程式的預期輸出:

Recovered connection: 3 matching rows; login time changed.

測試會終止一個被刻意閒置的工作階段。 它不會測試網路分割區、服務故障轉移、作用中交易復原、寫入重新執行或 Azure 限速。 不要把通過結果解讀為每個中斷的應用程式操作都可以安全重複的證據。

診斷失敗的復原

徵兆 支票
KILL 被拒絕 使用允許的測試身分,或請管理員執行受控測試。 不要改用其他工作階段 ID。
第一個查詢已經失敗了 在測試復原前,先先修正連線、憑證、查詢和權限。
KILL 之後查詢失敗 檢查 ConnectRetryCount查詢逾時、ODBC 診斷,以及會話狀態是否可恢復。 不要用無條件重試來掩蓋錯誤。
開啟新連線後,臨時資料表會消失 應用程式建立的替換連線不等同於原有工作階段。 重新設計依賴工作階段的作業,而非假設會自動重建狀態。
寫入作業會回傳連線錯誤 先確定結果再重試。 僅有客戶端錯誤並不能證明伺服器已經回滾了。

將診斷資訊保存在受保護的記錄中。 SQLSRV 會暴露一個錯誤集合,該集合可以包含多個記錄;PDO 透過例外提供 SQLSTATE 與驅動程式細節。 記錄操作過程及其逾時設定,但不記錄密碼或令牌。 欲了解更多資訊,請參閱 日誌活動。

目標適用性

ODBC 與 SQL 目標端執行自動重新連線。 PHP 傳遞設定並回報成功或失敗。 它不會重建暫存物件、還原應用程式的變數,或重播交易。

受控程式使用 SQL Server,並需透過第二條連線終止自身測試會話的權限。 Azure SQL Database 與 Fabric 中的 SQL 資料庫各自有其服務權限與認證要求。 不要把這些程式當作 Azure SQL 受控執行個體、Azure Synapse Analytics 或 Fabric 中 Warehouse 的可直接替代的測試,也不要從一般連線成功推斷它們的復原支援。 在依賴該服務之前,請先確認所選服務的恢復支援。 Fabric 中的倉庫與 SQL 資料庫仍是不同的目標。