

本文為英文版的機器翻譯版本，如內容有任何歧義或不一致之處，概以英文版為準。

# Amazon QLDB 驅動程式建議
<a name="driver.best-practices"></a>

**重要**  
支援終止通知：現有客戶將可以使用 Amazon QLDB，直到 07/31/2025 的支援結束為止。如需詳細資訊，請參閱[將 Amazon QLDB Ledger 遷移至 Amazon Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

本節說明針對任何支援的語言設定和使用 Amazon QLDB 驅動程式的最佳實務。提供的程式碼範例專為 Java 提供。

這些建議適用於大多數典型的使用案例，但單一大小並不適合所有案例。使用下列建議，因為您認為適合您的應用程式。

**Topics**
+ [設定 QldbDriver 物件](#driver.best-practices.configuring)
+ [對例外狀況重試](#driver.best-practices.retrying)
+ [最佳化效能](#driver.best-practices.performance)
+ [每個交易執行多個陳述式](#driver.best-practices.multiple-statements)

## 設定 QldbDriver 物件
<a name="driver.best-practices.configuring"></a>

`QldbDriver` 物件會透過維護跨交易重複使用的*工作階段*集區來管理與分類帳的連線。[工作階段](driver-session-management.md)代表與分類帳的單一連線。QLDB 支援每個工作階段一個主動執行的交易。

**重要**  
對於較舊的驅動程式版本，工作階段集區功能仍位於 `PooledQldbDriver` 物件中，而不是 `QldbDriver`。如果您使用的是下列其中一個版本，請將本主題其餘部分提及的 `QldbDriver` 取代`PooledQldbDriver`為 。  


****  

| 驅動程式 | 版本 | 
| --- | --- | 
| Java | 1.1.0 或更早版本 | 
| .NET | 0.1.0-beta | 
| Node.js | 1.0.0-rc.1 或更早的版本 | 
| Python | 2.0.2 或更早的版本 | 
`PooledQldbDriver` 物件在最新版的驅動程式中已棄用。我們建議您升級至最新版本，並將 的任何執行個體轉換為 `PooledQldbDriver` `QldbDriver`。

**將 QldbDriver 設定為全域物件**

若要最佳化驅動程式和工作階段的使用，請確定您的應用程式執行個體中只有一個驅動程式的全域執行個體。例如，在 Java 中，您可以使用*相依性注入*架構，例如 [Spring](https://spring.io/)、[Google Guice](https://github.com/google/guice) 或 [Dagger](https://dagger.dev/)。下列程式碼範例示範如何將 `QldbDriver`設定為單一程式碼。

```
@Singleton
public QldbDriver qldbDriver (AWSCredentialsProvider credentialsProvider,
                                    @Named(LEDGER_NAME_CONFIG_PARAM) String ledgerName) {
    QldbSessionClientBuilder builder = QldbSessionClient.builder();
    if (null != credentialsProvider) {
        builder.credentialsProvider(credentialsProvider);
    }
    return QldbDriver.builder()
            .ledger(ledgerName)
            .transactionRetryPolicy(RetryPolicy
                .builder()
                .maxRetries(3)
                .build())
            .sessionClientBuilder(builder)
            .build();
}
```

**設定重試嘗試**

當發生常見的暫時性例外狀況 （例如 `SocketTimeoutException`或 `NoHttpResponseException`) 時，驅動程式會自動重試交易。若要設定重試嘗試次數上限，您可以在建立 執行個體時使用`transactionRetryPolicy`組態物件的 `maxRetries` 參數`QldbDriver`。（對於上一節中列出的較舊驅動程式版本，請使用 的 `retryLimit` 參數`PooledQldbDriver`。)

`maxRetries` 的預設值為 `4`。

`InvalidParameterException` 無法重試 等用戶端錯誤。發生時，交易會中止，工作階段會傳回至集區，而例外狀況會擲回至驅動程式的用戶端。

**設定並行工作階段和交易的數量上限**

執行個體用來`QldbDriver`執行交易的分類帳工作階段數目上限是由其 `maxConcurrentTransactions` 參數所定義。（對於上一節中列出的較舊驅動程式版本，這由 `poolLimit` 參數 定義`PooledQldbDriver`。)

此限制必須大於零，並小於或等於工作階段用戶端允許的最大開放 HTTP 連線數，如特定 AWS SDK 所定義。例如，在 Java 中，連線數量上限是在 [ClientConfiguration](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/ClientConfiguration.html#getMaxConnections--) 物件中設定。

的預設值`maxConcurrentTransactions`是 AWS SDK 的最大連線設定。

當您`QldbDriver`在應用程式中設定 時，請考量下列擴展考量：
+ 您的集區應擁有至少與您計劃同時執行的交易數量相同的工作階段數量。
+ 在多執行緒模型中，主管執行緒委派給工作者執行緒，驅動程式的工作階段數量至少應該與工作者執行緒數量相同。否則，在尖峰負載時，執行緒會排隊等待可用的工作階段。
+ 每個分類帳並行作用中工作階段的服務限制定義於 中[Amazon QLDB 中的配額和限制](limits.md#limits.fixed)。請確定您設定的並行工作階段限制不會超過此限制，以用於所有用戶端的單一分類帳。

## 對例外狀況重試
<a name="driver.best-practices.retrying"></a>

重試 QLDB 中發生的例外狀況時，請考慮下列建議。

**在 OccConflictException 上重試**

當交易存取的資料自交易開始以來已變更時，會發生*樂觀並行控制* (OCC) 衝突例外狀況。QLDB 在嘗試遞交交易時擲回此例外狀況。驅動程式會重試交易，次數最多不超過`maxRetries`設定的次數。

如需 OCC 的詳細資訊，以及使用索引限制 OCC 衝突的最佳實務，請參閱 [Amazon QLDB 並行模型](concurrency.md)。

**在 QldbDriver 以外的其他例外狀況上重試**

若要在執行時間擲出自訂、應用程式定義的例外狀況時，在驅動程式外重試交易，您必須包裝交易。例如，在 Java 中，以下程式碼顯示如何使用 [Reslience4J](https://resilience4j.readme.io/) 程式庫在 QLDB 中重試交易。

```
private final RetryConfig retryConfig = RetryConfig.custom()
        .maxAttempts(MAX_RETRIES)
        .intervalFunction(IntervalFunction.ofExponentialRandomBackoff())
        // Retry this exception
        .retryExceptions(InvalidSessionException.class, MyRetryableException.class)
        // But fail for any other type of exception extended from RuntimeException
        .ignoreExceptions(RuntimeException.class)
        .build();

// Method callable by a client
public void myTransactionWithRetries(Params params) {
    Retry retry = Retry.of("registerDriver", retryConfig);

    Function<Params, Void> transactionFunction = Retry.decorateFunction(
            retry,
            parameters ->  transactionNoReturn(params));
    transactionFunction.apply(params);
}

private Void transactionNoReturn(Params params) {
    try (driver.execute(txn -> {
            // Transaction code
        });
    }
    return null;
}
```

**注意**  
在 QLDB 驅動程式之外重試交易會有乘數效果。例如，如果 `QldbDriver` 設定為重試三次，而自訂重試邏輯也重試三次，則相同的交易最多可以重試九次。

**使交易具有等冪性**

最佳實務是，讓您的寫入交易具有等冪性，以避免重試時發生任何非預期的副作用。如果交易可以多次執行，並且每次產生相同的結果，則表示交易是*等*冪的。

如需進一步了解，請參閱 [Amazon QLDB 並行模型](concurrency.md#concurrency.idempotent)。

## 最佳化效能
<a name="driver.best-practices.performance"></a>

若要在使用驅動程式執行交易時最佳化效能，請考量下列因素：
+ `execute` 操作一律對 QLDB 進行至少三次 `SendCommand` API 呼叫，包括下列命令：

  1. `StartTransaction`

  1. `ExecuteStatement`

     系統會針對您在 `execute` 區塊中執行的每個 PartiQL 陳述式叫用此命令。

  1. `CommitTransaction`

  計算應用程式的整體工作負載時，請考量 API 呼叫的總數。
+ 一般而言，我們建議從單一執行緒寫入器開始，並在單一交易中批次處理多個陳述式來最佳化交易。最大化交易大小、文件大小和每筆交易的文件數量配額，如 中所定義[Amazon QLDB 中的配額和限制](limits.md#limits.fixed)。
+ 如果批次處理不足以處理大型交易負載，您可以新增其他寫入器來嘗試多執行緒。不過，您應該仔細考慮文件和交易排序的應用程式需求，以及它帶來的額外複雜性。

## 每個交易執行多個陳述式
<a name="driver.best-practices.multiple-statements"></a>

如[上一節](#driver.best-practices.performance)所述，您可以執行每筆交易的多個陳述式，以最佳化應用程式的效能。在下列程式碼範例中，您會查詢資料表，然後在交易中更新該資料表中的文件。您可以透過將 lambda 表達式傳遞至 `execute`操作來執行此操作。

------
#### [ Java ]

```
// This code snippet is intentionally trivial. In reality you wouldn't do this because you'd
// set your UPDATE to filter on vin and insured, and check if you updated something or not.
public static boolean InsureCar(QldbDriver qldbDriver, final String vin) {
    final IonSystem ionSystem = IonSystemBuilder.standard().build();
    final IonString ionVin = ionSystem.newString(vin);

    return qldbDriver.execute(txn -> {
        Result result = txn.execute(
                "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE",
                ionVin);
        if (!result.isEmpty()) {
            txn.execute("UPDATE Vehicles SET insured = TRUE WHERE vin = ?", ionVin);
            return true;
        }
        return false;
    });
}
```

------
#### [ .NET ]

```
// This code snippet is intentionally trivial. In reality you wouldn't do this because you'd
// set your UPDATE to filter on vin and insured, and check if you updated something or not.
public static async Task<bool> InsureVehicle(IAsyncQldbDriver driver, string vin)
{
    ValueFactory valueFactory = new ValueFactory();
    IIonValue ionVin = valueFactory.NewString(vin);

    return await driver.Execute(async txn =>
    {
        // Check if the vehicle is insured.
        Amazon.QLDB.Driver.IAsyncResult result = await txn.Execute(
            "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", ionVin);

        if (await result.CountAsync() > 0)
        {
            // If the vehicle is not insured, insure it.
            await txn.Execute(
                "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", ionVin);
            return true;
        }
        return false;
    });
}
```

------
#### [ Go ]

```
// This code snippet is intentionally trivial. In reality you wouldn't do this because you'd
// set your UPDATE to filter on vin and insured, and check if you updated something or not.
func InsureCar(driver *qldbdriver.QLDBDriver, vin string) (bool, error) {
    insured, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {

        result, err := txn.Execute(
            "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin)
        if err != nil {
            return false, err
        }

        hasNext := result.Next(txn)
        if !hasNext && result.Err() != nil {
            return false, result.Err()
        }

        if hasNext {
            _, err = txn.Execute(
                "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin)
            if err != nil {
                return false, err
            }
            return true, nil
        }
        return false, nil
    })
    if err != nil {
        panic(err)
    }

    return insured.(bool), err
}
```

------
#### [ Node.js ]

```
// This code snippet is intentionally trivial. In reality you wouldn't do this because you'd
// set your UPDATE to filter on vin and insured, and check if you updated something or not.
async function insureCar(driver: QldbDriver, vin: string): Promise<boolean> {

    return await driver.executeLambda(async (txn: TransactionExecutor) => {
        const results: dom.Value[] = (await txn.execute(
            "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin)).getResultList();

        if (results.length > 0) {
            await txn.execute(
                "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin);
            return true;
        }
        return false;
    });
};
```

------
#### [ Python ]

```
# This code snippet is intentionally trivial. In reality you wouldn't do this because you'd
# set your UPDATE to filter on vin and insured, and check if you updated something or not.

def do_insure_car(transaction_executor, vin):
    cursor = transaction_executor.execute_statement(
        "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin)
    first_record = next(cursor, None)
    if first_record:
        transaction_executor.execute_statement(
            "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin)
        return True
    else:
        return False

def insure_car(qldb_driver, vin_to_insure):
    return qldb_driver.execute_lambda(
        lambda executor: do_insure_car(executor, vin_to_insure))
```

------

驅動程式`execute`的操作會隱含地在該工作階段中啟動工作階段和交易。您在 lambda 表達式中執行的每個陳述式都會包裝在交易中。所有陳述式執行後，驅動程式會自動遞交交易。如果任何陳述式在自動重試限制用盡之後失敗，則交易會中止。

**在交易中傳播例外狀況**

在每次交易執行多個陳述式時，通常不建議您在交易中擷取和吞下例外狀況。

例如，在 Java 中，下列程式會擷取 的任何執行個體`RuntimeException`、記錄錯誤，然後繼續。此程式碼範例被視為不良實務，因為即使`UPDATE`陳述式失敗，交易也會成功。因此，用戶端可能會假設更新在未成功時成功。

**警告**  
請勿使用此程式碼範例。它旨在顯示被視為不良做法的反模式範例。

```
// DO NOT USE this code example because it is considered bad practice
public static void main(final String... args) {
    ConnectToLedger.getDriver().execute(txn -> {
        final Result selectTableResult = txn.execute("SELECT * FROM Vehicle WHERE VIN ='123456789'");
        // Catching an error inside the transaction is an anti-pattern because the operation might
        // not succeed.
        // In this example, the transaction succeeds even when the update statement fails.
        // So, the client might assume that the update succeeded when it didn't.
        try {
            processResults(selectTableResult);
            String model = // some code that extracts the model
            final Result updateResult = txn.execute("UPDATE Vehicle SET model = ? WHERE VIN = '123456789'",
                    Constants.MAPPER.writeValueAsIonValue(model));
        } catch (RuntimeException e) {
            log.error("Exception when updating the Vehicle table {}", e.getMessage());
        }
    });
    log.info("Vehicle table updated successfully.");
}
```

請改用傳播 （向上產生氣泡） 例外狀況。如果交易的任何部分失敗，讓`execute`操作中止交易，以便用戶端可以相應地處理例外狀況。