J'ai un fil appelé T1
pour lire un fichier plat et l'analyser. Je dois créer un nouveau thread appelé T2
pour analyser une partie de ce fichier et plus tard ce T2
le thread devra mettre à jour le statut de l'entité d'origine, qui est également analysé et mis à jour par le thread d'origine T1
. Comment gérer cette situation?
Je reçois un fichier plat contenant les exemples d'enregistrements ci-dessous:
AAAA
BBBB
AACC
BBCC
AADD
BBDD
Ce fichier est d'abord enregistré dans la base de données dans l'état Received
. Désormais, tous les enregistrements commençant par BB
ou par AA
doivent être traités dans un thread séparé. Une fois l'analyse réussie, les deux threads tenteront de mettre à jour le statut de cet objet fichier dans une base de données vers Parsed
. Dans certains cas, j'obtiens staleObjectException
. Edit: Et le travail effectué par n'importe quel thread avant que l'exception ne soit perdue. Nous utilisons un verrouillage optimiste. Quelle est la meilleure façon d'éviter ce problème?
Exceptions d'hibernation possibles lorsque deux threads mettent à jour le même objet?
Le message ci-dessus aide à comprendre une partie de celui-ci, mais il n'aide pas à résoudre mon problème.
Partie 1 - Votre problème
La principale raison pour laquelle vous recevez cette exception est que vous utilisez Hibernate avec verrouillage optimiste . Cela vous dit essentiellement que le thread T1 ou le thread T2 ont déjà mis à jour l'état à [~ # ~] analysé [~ # ~] et maintenant l'autre thread contient l'ancienne version de la ligne avec version plus petite que celle contenue dans la base de données et essayant de mettre à jour l'état à [~ # ~] analysé [~ # ~] également.
La question ici est " Les deux threads essaient-ils de conserver les mêmes données ?". Si la réponse est oui, même si la dernière mise à jour réussit, il ne devrait pas y avoir de problème, car ils mettent finalement la ligne à jour dans le même état. Dans ce cas, vous n'avez pas besoin d'un verrouillage optimiste car vos données seront en tout cas synchronisées.
Le problème principal survient si après que l'état est défini sur [~ # ~] reçu [~ # ~] si les deux threads T1 et T2 dépendent réellement l'un de l'autre lors de la réinitialisation à l'état suivant. Dans ce cas, vous devez vous assurer que si T1 a été exécuté en premier (ou vice versa), T2 doit actualiser les données de la ligne mise à jour et réappliquer ses modifications en fonction des modifications déjà transmises par T1. Dans ce cas, la solution est la suivante. Si vous rencontrez staleObjectException, vous devez essentiellement actualiser vos données à partir de la base de données et redémarrer votre opération.
Analyse de la partie 2 sur le lien publié Exceptions d'hibernation possibles lorsque deux threads mettent à jour le même objet? Approche 1 , c'est plus ou moins la dernière mise à jour de la situation Wins . Il évite plus ou moins le verrouillage optimiste (le décompte des versions). Dans le cas où vous n'avez pas de dépendance de T1 à T2 ou inversé afin de définir l'état [~ # ~] analysé [~ # ~]. Ça devrait être bien.
Aproach 2 Optimistic Locking C'est ce que vous avez maintenant. La solution consiste à actualiser les données et à redémarrer votre opération.
Approche 3 Verrouillage DB de niveau ligne La solution ici est plus ou moins la même que pour l'approche 2 avec la petite correction que dure le verrouillage pessimiste. La principale différence est que dans ce cas, il peut s'agir d'un verrou READ et que vous ne pourrez même pas lire les données de la base de données afin de les actualiser s'il s'agit de PESSIMISTIC READ.
Synchronisation au niveau de l'application Aproach 4 Il existe de nombreuses manières différentes d'effectuer la synchronisation. Un exemple serait d'organiser réellement toutes vos mises à jour dans une file d'attente BlockingQueue ou JMS (si vous voulez qu'elle soit persistante) et de pousser toutes les mises à jour à partir d'un seul thread. Pour le visualiser un peu T1 et T2 mettront des éléments sur la file d'attente et il y aura une seule opération de lecture de thread T3 et les poussera vers le serveur de base de données.
Si vous utilisez la synchronisation au niveau de l'application, vous devez savoir que toutes les structures ne peuvent pas être distribuées dans un déploiement multi-serveur.
Et bien je ne peux penser à rien d'autre pour l'instant :)
Je ne suis pas certain de comprendre la question, mais il semble que cela constituerait une erreur logique pour un thread T1 qui ne traite, par exemple, que des enregistrements commençant par AA pour marquer le fichier entier comme "analysé"? Que se passe-t-il si, par exemple, votre application se bloque après les mises à jour T1 mais pendant que T2 traite toujours les enregistrements BB? Certains enregistrements BB sont susceptibles d'être perdus, n'est-ce pas?
Quoi qu'il en soit, le nœud du problème est que vous avez une condition de concurrence critique avec deux threads mettant à jour le même objet. L'exception d'objet périmé signifie simplement qu'un de vos threads a perdu la course. Une meilleure solution évite complètement une course.
(Je suppose ici que le traitement des enregistrements individuels est idempotent, si ce n'est pas le cas, je pense que vous avez de plus gros problèmes car certains modes de défaillance entraîneront le retraitement des enregistrements. Si le traitement des enregistrements doit avoir lieu une seule fois, alors vous ont un problème plus difficile pour lequel une file d'attente de messages serait probablement une meilleure solution.)
Je tirerais parti de la fonctionnalité de Java.util.concurrent pour envoyer des enregistrements aux travailleurs threadés et faire interagir le thread avec le bloc de mise en veille prolongée jusqu'à ce que tous les enregistrements aient été traités, auquel cas ce thread peut marquer le fichier comme "analysé".
Par exemple,
// do something like this during initialization, or use a Guava LoadingCache...
Map<RecordType, Executor> executors = new HashMap<>();
// note I'm assuming RecordType looks like an enum
executors.put(RecordType.AA_RECORD, Executors.newSingleThreadExecutor());
puis, au fur et à mesure que vous traitez le fichier, vous répartissez chaque enregistrement comme suit, en créant une liste de futurs correspondant à l'état des tâches en file d'attente. Supposons que le traitement réussi d'un enregistrement renvoie un booléen "true":
List<Future<Boolean>> tasks = new ArrayList<>();
for (Record record: file.getRecords()) {
Executor executorForRecord = executors.get(record.getRecordType());
tasks.add(executor.submit(new RecordProcessor(record)));
}
Attendez maintenant que toutes les tâches se terminent avec succès - il existe des moyens plus élégants de le faire, en particulier avec la goyave. Notez que vous devez également traiter ExecutionException ici si votre tâche a échoué avec une exception, je le passe en revue ici.
boolean allSuccess = true;
for (Future<Boolean> task: tasks) {
allSuccess = allSuccess && task.get();
if (!allSuccess) break;
}
// if all your tasks completed successfully, update the file record
if (allSuccess) {
file.setStatus("Parsed");
}
En supposant que chaque thread T1, T2 analyse différentes parties du fichier, cela signifie que personne ne remplace l'autre analyse de thread. la meilleure chose à faire est de découpler votre processus d'analyse de la validation de la base de données.
T1, T2 effectuera l'analyse T3 ou le thread principal effectuera la validation une fois les deux T1, T2 terminés. et je pense que dans cette approche, il est plus correct de changer l'état du fichier en Parsed
uniquement lorsque les deux threads sont terminés.
vous pouvez penser à T3 comme une classe CommitService qui attend jusqu'à T1, T2 finsih et ensuite se livrer à DB
CountDownLatch est un outil utile pour le faire. et voici un Exemple