web-dev-qa-db-fra.com

Spring JPA: Quel est le coût de saveandflush vs save?

J'ai une application construite à partir d'un ensemble de microservices. Un service reçoit des données, les conserve via le lien Spring JPA et Eclipse, puis envoie une alerte (AMQP) à un deuxième service.

En fonction de conditions spécifiques, le deuxième service appelle ensuite un service Web RESTfull sur les données persistantes pour récupérer les informations enregistrées.

J'ai remarqué que parfois le service RESTfull renvoie un ensemble de données nul même si les données ont été précédemment enregistrées. En regardant le code du service persistant, save a été utilisé au lieu de saveandflush donc je suppose que les données ne sont pas vidées assez rapidement pour que le service en aval puisse interroger.

  • Y a-t-il des coûts avec saveandflush dont je devrais être fatigué ou est-il raisonnable de l'utiliser par défaut?
  • Cela garantirait-il l'immédiateté de la disponibilité des données aux applications en aval?

Je dois dire que la fonction de persistance d'origine est enveloppée dans @Transactional

13
skyman

Pronostic possible du problème

Je crois que le problème ici n'a rien à voir avec save vs saveAndFlush. Le problème semble lié à la nature des méthodes Spring @Transactional Et à une utilisation incorrecte de ces transactions dans un environnement distribué impliquant à la fois votre base de données et un courtier AMQP; et peut-être, ajouter à ce mélange toxique, quelques malentendus de base sur le fonctionnement du contexte JPA.

Dans votre explication, vous semblez impliquer que vous démarrez votre transaction JPA dans une méthode @Transactional, Et pendant la transaction (mais avant qu'elle ne soit validée), vous envoyez des messages à un courtier AMQP; et plus tard, de l'autre côté de la file d'attente, une application cliente reçoit les messages et effectue un appel de service REST. À ce stade, vous remarquez que les modifications transactionnelles du côté éditeur n'ont pas encore ont été enregistrés dans la base de données et ne sont donc pas visibles pour le consommateur.

Le problème semble être que vous propagez ces messages AMQP dans votre transaction JPA avant qu'elle ne soit validée sur le disque. Au moment où le consommateur lit un message et le traite, votre transaction du côté de la publication n'est peut-être pas encore terminée. Ces modifications ne sont donc pas visibles pour l'application grand public.

Si votre implémentation AMPQ est Rabbit, j'ai déjà rencontré ce problème: lorsque vous démarrez une méthode @Transactional Qui utilise un gestionnaire de transactions de base de données, et dans cette méthode, vous utilisez un RabbitTemplate pour envoyer un correspondant message.

Si votre RabbitTemplate n'utilise pas de canal traité (c'est-à-dire channelTransacted=true), Votre message est remis avant que la transaction de base de données ne soit validée. Je crois qu'en activant les canaux traités (désactivés par défaut) dans votre RabbitTemplate, vous résolvez une partie du problème.

<rabbit:template id="rabbitTemplate" 
                 connection-factory="connectionFactory" 
                 channel-transacted="true"/>

Lorsque le canal est traité, le RabbitTemplate "rejoint" la transaction de base de données actuelle (qui est apparemment une transaction JPA). Une fois que votre transaction JPA est validée, elle exécute un code d'épilogue qui valide également les modifications dans votre canal Rabbit, ce qui force "l'envoi" réel du message.

À propos de save vs saveAndFlush

Vous pourriez penser que vider les changements dans votre contexte JPA aurait dû résoudre le problème, mais vous auriez tort. Le vidage de votre contexte JPA force simplement les modifications dans vos entités (à ce stade, juste en mémoire) à être écrites sur le disque, mais elles sont toujours écrites sur le disque dans une transaction de base de données correspondante, qui ne sera pas validée tant que votre transaction JPA ne sera pas validée. Cela se produit à la fin de votre méthode @Transactional (Et malheureusement quelque temps après avoir déjà envoyé vos messages AMQP - si vous n'utilisez pas un canal traité comme expliqué ci-dessus).

Ainsi, même si vous videz votre contexte JPA, votre application client ne verra pas ces modifications (selon les règles de niveau d'isolement de base de données classiques) jusqu'à ce que votre méthode @Transactional Soit terminée dans votre application d'éditeur.

Lorsque vous appelez save(entity), le EntityManager n'a pas besoin de synchroniser immédiatement les modifications. La plupart des implémentations JPA marquent simplement les entités comme sales en mémoire et attendent jusqu'à la dernière minute pour synchroniser toutes les modifications avec la base de données et valider ces modifications au niveau de la base de données.

Remarque: dans certains cas, vous souhaiterez peut-être que certaines de ces modifications soient immédiatement enregistrées sur le disque et pas avant que le fantaisiste EntityManager ne le décide. Un exemple classique de cela se produit lorsqu'il existe un déclencheur dans une table de base de données dont vous avez besoin pour exécuter afin de générer des enregistrements supplémentaires dont vous aurez besoin plus tard au cours de votre transaction. Vous forcez donc un vidage des modifications sur le disque de telle sorte que le déclencheur est forcé de s'exécuter.

En vidant le contexte, vous forcez simplement une synchronisation des modifications de la mémoire sur le disque, mais cela n'implique pas une validation instantanée de ces modifications dans la base de données. Par conséquent, les modifications que vous effectuez ne seront pas nécessairement visibles pour les autres transactions. Très probablement, ils ne le seront pas, sur la base des niveaux d'isolement de base de données traditionnels.

Le problème 2PC

Un autre problème classique ici est que votre base de données et votre courtier AMQP sont deux systèmes indépendants. S'il s'agit de Rabbit, vous n'avez pas de 2PC (validation en deux phases).

Donc, vous voudrez peut-être tenir compte de scénarios intéressants, par exemple votre transaction de base de données est validée, mais Rabbit ne parvient pas à valider votre message, auquel cas vous devrez répéter l'intégralité de la transaction, en ignorant éventuellement les effets secondaires de la base de données et en tentant à nouveau d'envoyer le message à Rabbit.

Vous devriez probablement lire cet article sur Transactions distribuées au printemps, avec et sans XA , en particulier la section sur les transactions en chaîne est utile pour résoudre ce problème.

Ils suggèrent une définition de gestionnaire de transactions plus complexe. Par exemple:

<bean id="jdbcTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

<bean id="rabbitTransactionManager" class="org.springframework.amqp.rabbit.transaction.RabbitTransactionManager">
    <property name="connectionFactory" ref="connectionFactory"/>
</bean>

<bean id="chainedTransactionManager" class="org.springframework.data.transaction.ChainedTransactionManager">
    <constructor-arg name="transactionManagers">
        <array>
            <ref bean="rabbitTransactionManager"/>
            <ref bean="jdbcTransactionManager"/>
        </array>
    </constructor-arg>
</bean>

Et puis, dans votre code, vous utilisez simplement ce gestionnaire de transactions chaîné pour coordonner les deux, votre partie transactionnelle de base de données et votre partie transactionnelle Rabbit.

Maintenant, il est possible que vous validiez votre partie de base de données, mais que votre partie de transaction Rabbit échoue.

Alors, imaginez quelque chose comme ça:

@Retry
@Transactional("chainedTransactionManager")
public void myServiceOperation() {
    if(workNotDone()) {
        doDatabaseTransactionWork();
    }
    sendMessagesToRabbit();
}

De cette manière, si votre partie transactionnelle Rabbit échouait pour une raison quelconque et que vous deviez réessayer la transaction chaînée en entier, vous éviteriez de répéter les effets secondaires de la base de données et vous assuriez simplement d'envoyer le message ayant échoué à Rabbit.

Dans le même temps, si votre partie de base de données échoue, vous n'avez jamais envoyé le message à Rabbit et il n'y aurait aucun problème.

Alternativement, si les effets secondaires de votre base de données sont idempotents, vous pouvez ignorer la vérification, réappliquer simplement les modifications de la base de données et réessayer d'envoyer le message à lapin.

La vérité est qu'initialement, ce que vous essayez de faire semble d'une simplicité trompeuse, mais une fois que vous explorez les différents problèmes et les comprenez, vous vous rendez compte que c'est une entreprise délicate de le faire de la bonne façon.

36
Edwin Dalorzo