Recomendaciones de controladores de Amazon QLDB - Amazon Quantum Ledger Database (Amazon QLDB)

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 controladores de Amazon QLDB

En esta sección se describen las prácticas recomendadas para configurar y utilizar el controlador Amazon QLDB para cualquier lenguaje 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.

Configuración del objeto QLDBDriver

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. QLDB admite 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 última versión y convertir cualquier instancia de PooledQldbDriver a QldbDriver.

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

Configurar 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 lado 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.

Configurar el 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 conexiones HTTP abiertas que permite el cliente de sesión, según lo definido en el SDK de AWS específico. Por ejemplo, en Java, el número máximo de conexiones se establece en el objeto ClientConfiguration.

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

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 de 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

Al volver a intentar las excepciones que se producen en la QLDB, tenga en cuenta las siguientes recomendaciones.

Reintentar en caso de OccConflictException

Las excepciones de conflicto relacionadas con el control de concurrencia optimista (OCC) se producen cuando los datos a los que accede la transacción han cambiado desde el inicio de la transacción. QLDB lanza esta excepción mientras intenta 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 la OCC y las prácticas recomendadas para utilizar índices para limitar los conflictos de OCC, consulte Modelo de concurrencia de Amazon QLDB.

Reintentar 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 reintentar una transacción en 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 QLDB tiene un efecto multiplicador. 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

Le recomendamos que haga que las 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 concurrencia de Amazon QLDB.

Optimización del rendimiento

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

  • La operación execute siempre realiza un mínimo de tres llamadas a la API SendCommand a QLDB, 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 llamadas a la API 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 de 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.