Recomendações de driver do Amazon QLDB - Amazon Quantum Ledger Database (Amazon QLDB)

As traduções são geradas por tradução automática. Em caso de conflito entre o conteúdo da tradução e da versão original em inglês, a versão em inglês prevalecerá.

Recomendações de driver do Amazon QLDB

Esta seção descreve as melhores práticas para configurar e usar o driver Amazon QLDB para qualquer linguagem compatível. Os exemplos de código fornecidos são especificamente para Java.

Essas recomendações se aplicam à maioria dos casos de uso comuns, mas um tamanho não serve para todos. Use as recomendações a seguir conforme achar adequado para seu aplicativo.

Configurando o objeto QldbDriver

O objeto QldbDriver gerencia as conexões com seu ledger mantendo um pool de sessões que são reutilizadas em todas as transações. Uma sessão representa uma única conexão com o ledger. O QLDB suporta uma transação em execução ativa por sessão.

Importante

Para versões mais antigas do driver, a funcionalidade de agrupamento de sessões ainda está no PooledQldbDriver objeto em vez de QldbDriver. Se você estiver usando uma das versões a seguir, substitua qualquer menção de QldbDriver por PooledQldbDriver por no restante deste tópico.

Driver Versão
Java 1.1.0 ou mais cedo
.NET 0.1.0-beta
Node.js 1.0.0-rc.1 ou mais cedo
Python 2.0.2 ou mais cedo

O objeto PooledQldbDriver está obsoleto na versão mais recente dos drivers. Recomendamos atualizar para a versão mais recente e converter todas as instâncias de PooledQldbDriver paraQldbDriver.

Configurar QlDBDriver como um objeto global

Para otimizar o uso de drivers e sessões, certifique-se de que exista apenas uma instância global do driver na instância do seu aplicativo. Por exemplo, em Java, você pode usar frameworks de injeção de dependência, como Spring, Google Guice ou Dagger. O exemplo de código a seguir mostra como 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 as tentativas de nova tentativa

O driver repete transações automaticamente quando ocorrem exceções transitórias comuns (como SocketTimeoutException ou NoHttpResponseException). Para definir o número máximo de tentativas de repetição, você pode usar o parâmetro maxRetries do objeto da configuração transactionRetryPolicy ao criar uma instância do QldbDriver. (Para versões mais antigas do driver, conforme listado na seção anterior, use o parâmetro retryLimit de PooledQldbDriver.)

O valor padrão de maxRetries é 4.

Erros do lado do cliente, como não é possível tentar InvalidParameterException novamente. Quando eles ocorrem, a transação é abortada, a sessão é retornada ao pool e a exceção é lançada para o cliente do driver.

Configurar o número máximo de sessões e transações simultâneos

O número máximo de sessões de ledger usadas por uma instância de QldbDriver para executar transações é definido por seu parâmetro maxConcurrentTransactions. (Para versões mais antigas do driver, conforme listado na seção anterior, isso é definido pelo parâmetro poolLimit de PooledQldbDriver.)

Esse limite deve ser maior que zero e menor ou igual ao número máximo de conexões HTTP abertas que o cliente da sessão permite, conforme definido pelo SDK AWS específico. Por exemplo, em Java, o número máximo de conexões é definido no objeto ClientConfiguration.

O valor padrão de maxConcurrentTransactions é a configuração máxima de conexão do seu SDK AWS.

Ao configurar o QldbDriver em seu aplicativo, considere as seguintes considerações de escalabilidade:

  • Seu pool sempre deve ter pelo menos tantas sessões quanto o número de transações em execução simultânea que você planeja ter.

  • Em um modelo multiencadeado em que um thread do supervisor delega aos threads do trabalhador, o driver deve ter pelo menos tantas sessões quanto o número de threads do trabalhador. Caso contrário, no pico de carga, os threads estarão esperando na fila por uma sessão disponível.

  • O limite de sessões ativas simultâneas por ledger é definido em Cotas e limites no Amazon QLDB. Certifique-se de não ter configurado mais do que esse limite de sessões simultâneas a serem usadas em um único ledger em todos os clientes.

Tentando novamente com exceções

Ao tentar novamente as exceções que ocorrem no QLDB, considere as recomendações a seguir.

Tentando novamente em OCCConflictException

As exceções de conflito de controle de simultaneidade otimista (OCC) ocorrem quando os dados que a transação está acessando são alterados desde o início da transação. O QLDB lança essa exceção ao tentar confirmar a transação. O driver repete a transação até quantas vezes maxRetries estiver configurada.

Para obter mais informações sobre OCC e as práticas recomendadas de uso de índices para limitar conflitos de OCC, consulte Modelo de simultaneidade do Amazon QLDB.

Tentando novamente em outras exceções fora do QlDBDriver

Para repetir uma transação fora do driver quando exceções personalizadas definidas pelo aplicativo são lançadas durante o runtime, você deve encapsular a transação. Por exemplo, em Java, o código a seguir mostra como usar a biblioteca Reslience4J para repetir uma transação no 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

Tentar novamente uma transação fora do driver QLDB tem um efeito multiplicador. Por exemplo, se QldbDriver estiver configurada para repetir três vezes e a lógica de repetição personalizada também tentar três vezes, a mesma transação poderá ser repetida até nove vezes.

Tornando as transações idempotentes

Como prática recomendada, torne suas transações de gravação idempotentes para evitar efeitos colaterais inesperados no caso de tentativas repetidas. Uma transação é idempotente se puder ser executada várias vezes e produzir resultados idênticos a cada vez.

Para saber mais, consulte Modelo de simultaneidade do Amazon QLDB.

Otimizar o desempenho

Para otimizar o desempenho ao executar transações usando o driver, considere as seguintes considerações:

  • A operação execute sempre faz no mínimo três SendCommand chamadas de API para o QLDB, incluindo os seguintes comandos:

    1. StartTransaction

    2. ExecuteStatement

      Esse comando é invocado para cada instrução partiQL que você executa no bloco execute.

    3. CommitTransaction

    Considere o número total de chamadas de API que são feitas quando você calcula a workload geral do seu aplicativo.

  • Em geral, recomendamos começar com um gravador de thread único e otimizar as transações agrupando várias declarações em lotes em uma única transação. Maximize as cotas de tamanho da transação, tamanho do documento e número de documentos por transação, conforme definido em Cotas e limites no Amazon QLDB.

  • Se o agrupamento em lotes não for suficiente para grandes cargas de transações, você pode tentar o multiencadeamento adicionando gravadores adicionais. No entanto, você deve considerar cuidadosamente os requisitos de seu aplicativo para sequenciamento de documentos e transações e a complexidade adicional que isso introduz.

Executando várias instruções por transação

Conforme descrito na seção anterior, você pode executar várias declarações por transação para otimizar o desempenho do seu aplicativo. No exemplo de código a seguir, você consulta uma tabela e, em seguida, atualiza um documento nessa tabela dentro de uma transação. Você faz isso passando uma expressão lambda para a operação 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))

A operação execute do driver inicia implicitamente uma sessão e uma transação nessa sessão. Cada instrução que você executa na expressão lambda é encapsulada na transação. Depois que todas as instruções são executadas, o driver confirma automaticamente a transação. Se alguma instrução falhar após o limite de repetição automática ser esgotado, a transação será abortada.

Propagar exceções em uma transação

Ao executar várias instruções por transação, geralmente não recomendamos que você capture e engula exceções dentro da transação.

Por exemplo, em Java, o programa a seguir captura qualquer instância de RuntimeException, registra o erro e continua. Esse exemplo de código é considerado uma prática ruim porque a transação é bem-sucedida mesmo quando a instrução UPDATE falha. Portanto, o cliente pode presumir que a atualização foi bem-sucedida quando não foi.

Atenção

Não use esse exemplo de código. É fornecido para mostrar um exemplo de antipadrão que é considerado uma má prática.

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

Em vez disso, propague (aumente) a exceção. Se alguma parte da transação falhar, deixe a operação execute abortar a transação para que o cliente possa lidar com a exceção adequadamente.