Meilleures pratiques Neptune avec OpenCypher et Bolt - Amazon Neptune

Les traductions sont fournies par des outils de traduction automatique. En cas de conflit entre le contenu d'une traduction et celui de la version originale en anglais, la version anglaise prévaudra.

Meilleures pratiques Neptune avec OpenCypher et Bolt

Suivez ces bonnes pratiques lors de l'utilisation du langage de requête OpenCypher et du protocole Bolt avec Neptune. Pour de plus amples informations sur l'utilisation d'OpenCypher dans Neptune, consultezAccès au graphe Neptune avec OpenCypher.

Neptune ne prend pas en charge plusieurs requêtes simultanées dans une transaction

Bien que le pilote Bolt lui-même autorise des requêtes simultanées dans une transaction, Neptune ne prend pas en charge plusieurs requêtes dans une transaction exécutée simultanément. Neptune exige plutôt que plusieurs requêtes d'une transaction soient exécutées séquentiellement et que les résultats de chaque requête soient complètement consommés avant le lancement de la requête suivante.

L'exemple ci-dessous montre comment utiliser Bolt pour exécuter plusieurs requêtes séquentiellement dans une transaction, de sorte que les résultats de chacune d'elles soient complètement consommés avant le début de la prochaine requête :

final String query = "MATCH (n) RETURN n"; try (Driver driver = getDriver(HOST_BOLT, getDefaultConfig())) { try (Session session = driver.session(readSessionConfig)) { try (Transaction trx = session.beginTransaction()) { final Result res_1 = trx.run(query); Assert.assertEquals(10000, res_1.list().size()); final Result res_2 = trx.run(query); Assert.assertEquals(10000, res_2.list().size()); } } }

Création d'une nouvelle connexion après basculement

En cas de basculement, le pilote Bolt peut continuer à se connecter à l'ancienne instance de graveur plutôt qu'à la nouvelle instance active, car le nom DNS est résolu à une adresse IP spécifique.

Pour éviter cela, fermez puis reconnectez leDriverobjet après tout basculement.

Gestion des connexions pour les applications à longue durée de vie

Lorsque vous créez des applications de longue durée, telles que celles exécutées dans des conteneurs ou sur des instances Amazon EC2, instanciez unDriverobjet une fois, puis réutilisez cet objet pendant toute la durée de vie de l'application. L'objet Driver est thread-safe, et la surcharge entraînée par l'initialisation est considérable.

Gestion des connexions pourAWS Lambda

Les tournevis ne sont pas recommandés pour une utilisation dansAWS Lambdafonctions, en raison de leurs frais généraux de connexion et de leurs exigences de gestion. Utilisation de l'Point de terminaison HTTPSà la place.

Fermez les objets du pilote lorsque vous avez terminé

Assurez-vous de fermer le client lorsque vous n'en avez plus besoin afin que les connexions Bolt soient fermées par le serveur et que toutes les ressources associées aux connexions soient publiées. Cela se produit automatiquement si vous fermez le pilote à l'aide dedriver.close().

Si le pilote n'est pas correctement fermé, Neptune met fin à toutes les connexions Bolt inactives après 20 minutes ou après 10 jours si vous utilisez l'authentification IAM.

Neptune ne prend pas en charge plus de 1000 connexions Bolt simultanées. Si vous ne fermez pas explicitement les connexions lorsque vous n'en avez plus besoin et que le nombre de connexions actives atteint cette limite de 1000, toutes les nouvelles tentatives de connexion échouent.

Utiliser des modes de transaction explicites pour la lecture et l'écriture

Lorsque vous utilisez des transactions avec Neptune et le pilote Bolt, il est préférable de définir explicitement le mode d'accès pour les transactions en lecture et en écriture sur les bons paramètres.

Transactions en lecture seule

Pour les transactions en lecture seule, si vous ne transmettez pas la configuration du mode d'accès appropriée lors de la création de la session, le niveau d'isolation par défaut est utilisé, c'est-à-dire l'isolation des requêtes de mutation. Par conséquent, il est important que les transactions en lecture seule définissent le mode d'accès surreadexplicitement.

Exemple de transaction en lecture de validation automatique :

SessionConfig sessionConfig = SessionConfig .builder() .withFetchSize(1000) .withDefaultAccessMode(AccessMode.READ) .build(); Session session = driver.session(sessionConfig); try { (Add your application code here) } catch (final Exception e) { throw e; } finally { driver.close() }

Exemple de transaction en lecture :

Driver driver = GraphDatabase.driver(url, auth, config); SessionConfig sessionConfig = SessionConfig .builder() .withDefaultAccessMode(AccessMode.READ) .build(); driver.session(sessionConfig).readTransaction( new TransactionWork<List<String>>() { @Override public List<String> execute(org.neo4j.driver.Transaction tx) { (Add your application code here) } } );

Dans les deux cas,SNAPSHOTisolationest réalisé à l'aide deSémantique des transactions en lecture seule Neptune.

Étant donné que les réplicas en lecture acceptent uniquement les requêtes en lecture seule, toutes les requêtes envoyées à un réplica en lecture s'exécutent sousSNAPSHOTSémantique d'isolement.

Il n'y a pas de lectures valides ou de lectures non répétables pour les transactions en lecture seule.

Transactions en lecture seule

Pour les requêtes de mutation, il existe trois mécanismes différents pour créer une transaction d'écriture, chacun étant illustré ci-dessous :

Exemple de transaction d'écriture implicite :

Driver driver = GraphDatabase.driver(url, auth, config); SessionConfig sessionConfig = SessionConfig .builder() .withDefaultAccessMode(AccessMode.WRITE) .build(); driver.session(sessionConfig).writeTransaction( new TransactionWork<List<String>>() { @Override public List<String> execute(org.neo4j.driver.Transaction tx) { (Add your application code here) } } );

Exemple de transaction d'écriture de validation automatique :

SessionConfig sessionConfig = SessionConfig .builder() .withFetchSize(1000) .withDefaultAccessMode(AccessMode.Write) .build(); Session session = driver.session(sessionConfig); try { (Add your application code here) } catch (final Exception e) { throw e; } finally { driver.close() }

Exemple de transaction d'écriture explicite :

Driver driver = GraphDatabase.driver(url, auth, config); SessionConfig sessionConfig = SessionConfig .builder() .withFetchSize(1000) .withDefaultAccessMode(AccessMode.WRITE) .build(); Transaction beginWriteTransaction = driver.session(sessionConfig).beginTransaction(); (Add your application code here) beginWriteTransaction.commit(); driver.close();

Niveaux d'isolation des transactions en écriture

  • Les lectures effectuées dans le cadre de requêtes de mutation sont exécutées sousREAD COMMITTEDl'isolement des transactions.

  • Il n'y a pas de lecture sale pour les lectures effectuées dans le cadre de requêtes de mutation.

  • Les enregistrements et les plages d'enregistrements sont verrouillés lors de la lecture dans une requête de mutation.

  • Lorsqu'une plage de l'index a été lue par une transaction de mutation, vous bénéficiez de la garantie que cette plage ne sera pas modifiée par des transactions simultanées jusqu'à la fin de la lecture.

Les requêtes de mutation ne sont pas thread-safe.

Pour les conflits, voirRésolution des conflits à l'aide de délais d'attente de verrouillage.

Les requêtes de mutation ne sont pas automatiquement réessayées en cas de défaillance.

Logique de nouvelle tentative pour les exceptions

Pour toutes les exceptions autorisant une nouvelle tentative, il est généralement préférable d'utiliser unstratégie exponentielle de backoff et de nouvelle tentativequi offre des temps d'attente progressivement plus longs entre les nouvelles tentatives afin de mieux gérer les problèmes transitoires tels queConcurrentModificationExceptionErreurs. L'exemple suivant illustre un modèle exponentiel de retour et de nouvelle tentative :

public static void main() { try (Driver driver = getDriver(HOST_BOLT, getDefaultConfig())) { retriableOperation(driver, "CREATE (n {prop:'1'})") .withRetries(5) .withExponentialBackoff(true) .maxWaitTimeInMilliSec(500) .call(); } } protected RetryableWrapper retriableOperation(final Driver driver, final String query){ return new RetryableWrapper<Void>() { @Override public Void submit() { log.info("Performing graph Operation in a retry manner......"); try (Session session = driver.session(writeSessionConfig)) { try (Transaction trx = session.beginTransaction()) { trx.run(query).consume(); trx.commit(); } } return null; } @Override public boolean isRetryable(Exception e) { if (isCME(e)) { log.debug("Retrying on exception.... {}", e); return true; } return false; } private boolean isCME(Exception ex) { return ex.getMessage().contains("Operation failed due to conflicting concurrent operations"); } }; } /** * Wrapper which can retry on certain condition. Client can retry operation using this class. */ @Log4j2 @Getter public abstract class RetryableWrapper<T> { private long retries = 5; private long maxWaitTimeInSec = 1; private boolean exponentialBackoff = true; /** * Override the method with custom implementation, which will be called in retryable block. */ public abstract T submit() throws Exception; /** * Override with custom logic, on which exception to retry with. */ public abstract boolean isRetryable(final Exception e); /** * Define the number of retries. * * @param retries -no of retries. */ public RetryableWrapper<T> withRetries(final long retries) { this.retries = retries; return this; } /** * Max wait time before making the next call. * * @param time - max polling interval. */ public RetryableWrapper<T> maxWaitTimeInMilliSec(final long time) { this.maxWaitTimeInSec = time; return this; } /** * ExponentialBackoff coefficient. */ public RetryableWrapper<T> withExponentialBackoff(final boolean expo) { this.exponentialBackoff = expo; return this; } /** * Call client method which is wrapped in submit method. */ public T call() throws Exception { int count = 0; Exception exceptionForMitigationPurpose = null; do { final long waitTime = exponentialBackoff ? Math.min(getWaitTimeExp(retries), maxWaitTimeInSec) : 0; try { return submit(); } catch (Exception e) { exceptionForMitigationPurpose = e; if (isRetryable(e) && count < retries) { Thread.sleep(waitTime); log.debug("Retrying on exception attempt - {} on exception cause - {}", count, e.getMessage()); } else if (!isRetryable(e)) { log.error(e.getMessage()); throw new RuntimeException(e); } } } while (++count < retries); throw new IOException(String.format( "Retry was unsuccessful.... attempts %d. Hence throwing exception " + "back to the caller...", count), exceptionForMitigationPurpose); } /* * Returns the next wait interval, in milliseconds, using an exponential backoff * algorithm. */ private long getWaitTimeExp(final long retryCount) { if (0 == retryCount) { return 0; } return ((long) Math.pow(2, retryCount) * 100L); } }