Recomendaciones de QLDB conductores de Amazon - Base de datos Amazon Quantum Ledger (AmazonQLDB)

Las traducciones son generadas a través de traducción automática. En caso de conflicto entre la traducción y la version original de inglés, prevalecerá la version en inglés.

Recomendaciones de QLDB conductores de Amazon

importante

Aviso de fin del soporte: los clientes actuales podrán utilizar Amazon QLDB hasta que finalice el soporte, el 31 de julio de 2025. Para obtener más información, consulte Migración de un Amazon QLDB Ledger a Amazon Aurora SQL Postgre.

En esta sección se describen las prácticas recomendadas para configurar y usar el QLDB controlador de Amazon para cualquier idioma compatible. Los ejemplos de código proporcionados son específicos para Java.

Estas recomendaciones se aplican a la mayoría de los casos de uso típicos, pero no hay una solución única para todos. Utilice las siguientes recomendaciones como mejor le parezca para su aplicación.

Configurar el QldbDriver objeto

El objeto QldbDriver administras las conexiones a su libro mayor manteniendo un conjunto de sesiones que se reutilizan en todas las transacciones. Una sesión representa una conexión única con el libro mayor. QLDBadmite una transacción en ejecución activa por sesión.

importante

En las versiones anteriores del controlador, la funcionalidad de agrupación de sesiones sigue estando en el objeto PooledQldbDriver y no en QldbDriver. Si utiliza una de las siguientes versiones, sustituya cualquier mención de QldbDriver por PooledQldbDriver para el resto de este tema.

Controlador Versión
Java 1.1.0 o anterior
.NET 0.1.0-beta
Node.js 1.0.0-rc.1 o anterior
Python 2.0.2 o anterior

El objeto PooledQldbDriver está obsoleto en la versión más reciente de los controladores. Se recomienda actualizar a la versión más reciente y convertir cualquier instancia de PooledQldbDriver a QldbDriver.

QldbDriver Configúralo como un objeto global

Para optimizar el uso de los controladores y las sesiones, asegúrese de que solo exista una instancia global del controlador en la instancia de la aplicación. Por ejemplo, en Java, puede usar marcos de inyección de dependencias como Spring, Google Guice o Dagger. En el ejemplo de código siguiente se muestra cómo configurar QldbDriver como singleton.

@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(); }

Configuración de los reintentos

El controlador vuelve a intentar las transacciones automáticamente cuando se producen excepciones transitorias comunes (por ejemplo, SocketTimeoutException o NoHttpResponseException). Para establecer el número máximo de reintentos, puede utilizar el parámetro maxRetries del objeto de configuración transactionRetryPolicy al crear una instancia de QldbDriver. (Para versiones anteriores del controlador, como se indica en la sección anterior, utilice el parámetro retryLimit de PooledQldbDriver.)

El valor predeterminado de maxRetries es 4.

Errores del cliente, como InvalidParameterException no se pueden volver a intentar. Cuando se producen, la transacción se cancela, la sesión se devuelve al grupo y la excepción se envía al cliente del controlador.

Configuración del número máximo de sesiones y transacciones simultáneas

El número máximo de sesiones de libro mayor que utiliza una instancia QldbDriver para ejecutar transacciones viene definido por su parámetro maxConcurrentTransactions. (Para versiones anteriores del controlador, como se indica en la sección anterior, se define en el parámetro poolLimit de PooledQldbDriver.)

Este límite debe ser superior a cero e inferior o igual al número máximo de HTTP conexiones abiertas que permite el cliente de sesión, según lo defina la especificación AWS SDK. Por ejemplo, en Java, el número máximo de conexiones se establece en el ClientConfigurationobjeto.

El valor predeterminado de maxConcurrentTransactions es la configuración de conexión máxima de su AWS SDK.

Cuando configure QldbDriver en su aplicación, tenga en cuenta las siguientes consideraciones de escalado:

  • Su grupo siempre debe tener al menos tantas sesiones como el número de transacciones en ejecución simultánea que planea tener.

  • En un modelo de subprocesos múltiples en el que un subproceso supervisor delega en subprocesos de trabajo, el controlador debe tener al menos tantas sesiones como el número de subprocesos de trabajo. De lo contrario, en el momento de máxima carga, los subprocesos estarán esperando en fila hasta que haya una sesión disponible.

  • El límite de servicio de sesiones activas simultáneas por libro mayor se define en Cuotas y límites en Amazon QLDB. Asegúrese de no haber configurado un número de sesiones simultáneas superior a este límite para utilizarlas en un solo libro mayor en todos los clientes.

Reintentar en caso de excepciones

Cuando vuelva a intentarlo con las excepciones que se produzcan enQLDB, tenga en cuenta las siguientes recomendaciones.

Intentándolo de nuevo OccConflictException

Las excepciones de conflicto del control de simultaneidad optimista (OCC) se producen cuando los datos a los que accede la transacción han cambiado desde el inicio de la transacción. QLDBlanza esta excepción al intentar confirmar la transacción. El controlador vuelve a intentar la transacción tantas veces como se haya configurado en maxRetries.

Para obtener más información sobre el uso de índices OCC y las prácticas recomendadas para limitar OCC los conflictos, consulteModelo de QLDB simultaneidad de Amazon.

Volver a intentarlo con otras excepciones fuera de QldbDriver

Para reintentar una transacción fuera del controlador cuando se producen excepciones personalizadas y definidas por la aplicación durante el tiempo de ejecución, debe empaquetar la transacción. Por ejemplo, en Java, el código siguiente muestra cómo utilizar la biblioteca Reslience4J para volver a intentar una transacción. 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; }
nota

Reintentar una transacción fuera del controlador tiene un efecto multiplicadorQLDB. Por ejemplo, si QldbDriver está configurado para volver a intentarlo tres veces y la lógica de reintento personalizada también lo hace tres veces, se puede volver a intentar la misma transacción hasta nueve veces.

Hacer que las transacciones sean idempotentes

Como práctica recomendada, haga que sus transacciones de escritura sean idempotentes para evitar cualquier efecto secundario inesperado en caso de reintentos. Una transacción es idempotente si puede ejecutarse varias veces y producir resultados idénticos cada vez.

Para obtener más información, consulte Modelo de QLDB simultaneidad de Amazon.

Optimización del rendimiento

Para optimizar el rendimiento al ejecutar transacciones con el controlador, tenga en cuenta las siguientes consideraciones:

  • La execute operación siempre realiza un mínimo de tres SendCommand API llamadas aQLDB, incluidos los siguientes comandos:

    1. StartTransaction

    2. ExecuteStatement

      Este comando se invoca para cada instrucción PartiQL que ejecute en el bloque execute.

    3. CommitTransaction

    Tenga en cuenta el número total de API llamadas que se realizan al calcular la carga de trabajo total de la aplicación.

  • En general, se recomienda empezar con un escritor de un solo subproceso y optimizar las transacciones agrupando varias instrucciones en una sola transacción. Maximice las cuotas de tamaño de transacción, tamaño de documento y cantidad de documentos por transacción, tal y como se define en Cuotas y límites en Amazon QLDB.

  • Si el procesamiento por lotes no es suficiente para grandes cargas de transacciones, puede probar con varios subprocesos añadiendo instancias de escritura adicionales. Sin embargo, debe considerar detenidamente los requisitos de su solicitud para la secuenciación de documentos y transacciones y la complejidad adicional que esto implica.

Ejecutar varias instrucciones por transacción

Como se describe en la sección anterior, puede ejecutar varias instrucciones por transacción para optimizar el rendimiento de su aplicación. En el siguiente ejemplo de código, se consulta una tabla y, a continuación, se actualiza un documento de esa tabla dentro de una transacción. Para ello, debe pasar una expresión lambda a la operación 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))

La operación execute del controlador inicia implícitamente una sesión y una transacción en esa sesión. Cada instrucción que se ejecuta en la expresión lambda se incluye en la transacción. Una vez ejecutadas todas las instrucciones, el controlador confirma automáticamente la transacción. Si alguna instrucción falla una vez agotado el límite de reintentos automáticos, la transacción se cancela.

Propagar las excepciones en una transacción

Cuando se ejecutan varias instrucciones por transacción, por lo general, no recomendamos detectar excepciones dentro de la transacción.

Por ejemplo, en Java, el siguiente programa detecta cualquier instancia de RuntimeException, registra el error y continúa. Este ejemplo de código se considera una mala práctica porque la transacción se realiza correctamente incluso cuando la instrucción UPDATE falla. Por lo tanto, el cliente podría suponer que la actualización se realizó correctamente, pero no fue así.

aviso

No utilice este ejemplo de código. Se proporciona para mostrar un ejemplo anti-patrón que se considera una mala práctica.

// 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."); }

En su lugar, propague (haga crecer) la excepción. Si alguna parte de la transacción falla, deje que la operación execute cancele la transacción para que el cliente pueda gestionar la excepción en consecuencia.