Supposons que nous ayons un microservices Utilisateur, Portefeuille REST et une passerelle API qui collent les éléments. Lorsque Bob s'inscrit sur notre site Web, notre passerelle API doit créer un utilisateur via le microservice de l'utilisateur et un portefeuille via le microservice de portefeuille.
Maintenant, voici quelques scénarios où les choses pourraient mal se passer:
La création de l'utilisateur Bob échoue: c'est correct, nous renvoyons simplement un message d'erreur à Bob. Nous utilisons des transactions SQL afin que personne n'ait jamais vu Bob dans le système. Tout est bien :)
L'utilisateur Bob est créé, mais avant que notre portefeuille puisse être créé, notre passerelle API se bloque. Nous avons maintenant un utilisateur sans portefeuille (données incohérentes).
L'utilisateur Bob est créé et, lors de la création du portefeuille, la connexion HTTP est interrompue. La création du portefeuille peut avoir réussi ou non.
Quelles solutions sont disponibles pour éviter ce type d'incohérence dans les données? Existe-t-il des modèles permettant aux transactions de s'étendre sur plusieurs requêtes REST? J'ai lu la page Wikipedia sur Le commit en deux phases qui semble toucher à cette question, mais je ne sais pas comment l'appliquer dans la pratique. This Atomic Distributed Transactions: un projet RESTful papier semble également intéressant bien que je ne l’aie pas encore lu.
Sinon, je sais que REST pourrait ne pas convenir à ce cas d'utilisation. La bonne façon de gérer cette situation serait-elle de supprimer complètement le [REST] _ et d'utiliser un protocole de communication différent, comme un système de file d'attente de messages? Ou devrais-je imposer la cohérence dans mon code d'application (par exemple, en ayant un travail d'arrière-plan qui détecte les incohérences et les corrige ou en ayant un attribut "state" sur mon modèle utilisateur avec des valeurs "creation", "créé", etc.)?
Ce qui n'a pas de sens:
Qu'est-ce qui va vous donner des maux de tête:
Quelle est probablement la meilleure alternative:
Mais si vous avez besoin de réponses synchrones?
Tous les systèmes distribués ont des problèmes de cohérence transactionnelle. La meilleure façon de faire est, comme vous l'avez dit, d'avoir un commit en deux phases. Demandez au portefeuille et à l'utilisateur d'être créés dans un état en attente. Une fois créé, effectuez un appel séparé pour activer l'utilisateur.
Ce dernier appel doit pouvoir être répété en toute sécurité (au cas où votre connexion serait interrompue).
Cela nécessitera que le dernier appel connaisse les deux tables (pour pouvoir le faire dans une seule transaction JDBC).
Sinon, vous voudrez peut-être réfléchir à la raison pour laquelle vous êtes si inquiet au sujet d'un utilisateur sans portefeuille. Croyez-vous que cela va causer un problème? Si tel est le cas, peut-être que les appels en repos séparés sont une mauvaise idée. Si un utilisateur ne doit pas exister sans portefeuille, vous devez probablement l'ajouter à l'utilisateur (dans l'appel d'origine POST pour créer l'utilisateur).
IMHO L'un des aspects clés de l'architecture des microservices est que la transaction est limitée au microservice individuel (principe de responsabilité unique).
Dans l'exemple actuel, la création de l'utilisateur serait une transaction propre. La création de l'utilisateur pousserait un événement USER_CREATED dans une file d'attente d'événements. Le service de portefeuille s'abonnerait à l'événement USER_CREATED et créerait le portefeuille.
Si mon portefeuille était simplement un autre groupe d'enregistrements dans la même base de données SQL que l'utilisateur, je placerais probablement le code de création d'utilisateur et de portefeuille dans le même service et le gérerais à l'aide des installations de transaction de base de données normales.
Il me semble que vous demandez ce qui se passe lorsque le code de création de portefeuille nécessite de toucher un ou plusieurs autres systèmes. Je dirais que tout dépend de la complexité et des risques du processus de création.
S'il s'agit simplement de toucher un autre magasin de données fiable (par exemple, un magasin qui ne peut pas participer à vos transactions SQL), alors, en fonction des paramètres système globaux, je serais peut-être prêt à prendre le risque infime que la deuxième écriture ne se produise pas. Je pourrais ne rien faire, mais lever une exception et traiter les données incohérentes via une transaction de compensation ou même une méthode ad hoc. Comme je le dis toujours à mes développeurs, "si ce genre de chose se produit dans l'application, cela ne passera pas inaperçu".
À mesure que la complexité et les risques liés à la création de portefeuilles augmentent, vous devez prendre des mesures pour réduire les risques. Supposons que certaines étapes nécessitent l’appel de plusieurs partenaires.
À ce stade, vous pouvez introduire une file de messages avec la notion d’utilisateurs et/ou de portefeuilles partiellement construits.
Une stratégie simple et efficace pour vous assurer que vos entités sont finalement bien construites consiste à faire en sorte que les tâches soient réessayées jusqu'à ce qu'elles aboutissent, mais cela dépend en grande partie des cas d'utilisation de votre application.
Je voudrais aussi réfléchir longuement à la raison pour laquelle j'ai eu une étape sujette aux échecs dans mon processus de provisioning.
Quelles solutions sont disponibles pour éviter ce type d'incohérence dans les données?
Traditionnellement, les gestionnaires de transactions distribuées sont utilisés. Il y a quelques années, dans le monde Java EE, vous auriez peut-être créé ces services sous la forme de EJB s déployés sur différents nœuds et votre passerelle d'API aurait effectué des appels à distance vers ces EJB. Le serveur d'applications (s'il est configuré correctement) garantit automatiquement, à l'aide de la validation en deux phases, que la transaction est validée ou annulée sur chaque nœud, afin de garantir la cohérence. Mais pour cela, tous les services doivent être déployés sur le même type de serveur d’applications (afin qu’ils soient compatibles) et n’ont en réalité jamais fonctionné qu’avec des services déployés par une seule entreprise.
Existe-t-il des modèles autorisant les transactions à couvrir plusieurs demandes REST?
Pour SOAP (ok, pas REST), il y a la spécification WS-AT mais aucun service que j'ai jamais eu à intégrer n'a pris en charge cela. Pour REST, JBoss a quelque chose en cours . Sinon, le "modèle" consiste à trouver un produit que vous pouvez connecter à votre architecture ou à créer votre propre solution (non recommandé).
J'ai publié un tel produit pour Java EE: https://github.com/maxant/genericconnector
Selon le document que vous avez référencé, il existe également le modèle Essayer d’annuler/confirmer et le produit associé d’Atomikos.
Les moteurs BPEL gèrent la cohérence entre les services déployés à distance à l'aide de la compensation.
Sinon, je sais que REST pourrait ne pas convenir à ce cas d'utilisation. La bonne façon de gérer cette situation serait-elle d’abandonner complètement REST et d’utiliser un protocole de communication différent, comme un système de file d’attente?
Il existe de nombreuses manières de "lier" des ressources non transactionnelles à une transaction:
Ou devrais-je imposer la cohérence dans mon code d'application (par exemple, en ayant un travail d'arrière-plan qui détecte les incohérences et les corrige ou en ayant un attribut "state" sur mon modèle utilisateur avec des valeurs "creation", "created", etc.)?
Jouer au diable comme avocat: pourquoi construire quelque chose comme ça, alors qu'il existe des produits qui le font pour vous (voir ci-dessus), et le font probablement mieux que vous ne pouvez, parce qu'ils ont fait leurs preuves?
Personnellement, j'aime bien l'idée de Micro Services, des modules définis par les cas d'utilisation, mais comme votre question le mentionne, ils ont des problèmes d'adaptation pour les entreprises classiques telles que les banques, les assurances, les télécoms, etc.
Les transactions distribuées, comme beaucoup l'ont mentionné, ne constituent pas un bon choix. Les utilisateurs préfèrent désormais des systèmes cohérents, mais je ne suis pas sûr que cela fonctionnera pour les banques, les assurances, etc.
J'ai écrit un blog sur ma solution proposée, peut-être que cela peut vous aider ....
Une solution simple consiste à créer un utilisateur à l'aide du service utilisateur et à utiliser un bus de messagerie dans lequel le service utilisateur émet ses événements. Le service Wallet s'enregistre sur le bus de messagerie, écoute l'événement créé par l'utilisateur et crée un portefeuille pour l'utilisateur. En attendant, si l'utilisateur va sur l'interface utilisateur du portefeuille pour voir son portefeuille, vérifiez si l'utilisateur vient d'être créé et s'il montre que la création de votre portefeuille est en cours, veuillez vous enregistrer un peu plus tard.
La cohérence éventuelle est la clé ici.
Le commandant est responsable de la transaction distribuée et en prend le contrôle. Il connaît l'instruction à exécuter et coordonnera son exécution. Dans la plupart des scénarios, il n'y aura que deux instructions, mais il peut gérer plusieurs instructions.
Le commandant prend la responsabilité de garantir l'exécution de toutes les instructions, ce qui signifie qu'il se retire. Lorsque le commandant de bord essaie d’effectuer la mise à jour à distance et n’obtient pas de réponse, il n’a plus de tentative. Ainsi, le système peut être configuré pour être moins sujet aux défaillances et il se soigne lui-même.
Comme nous avons de nouvelles tentatives, nous avons idempotence. Idempotence est la propriété de pouvoir faire quelque chose deux fois de telle sorte que les résultats finaux soient les mêmes que si cela avait été fait une seule fois. Nous avons besoin d’idempotence sur le service distant ou la source de données pour que, dans le cas où il reçoit l’instruction plusieurs fois, il ne la traite qu’une seule fois.
Cohérence éventuelle. Ceci résout la plupart des problèmes liés aux transactions distribuées, mais nous devons examiner quelques points ici. Chaque transaction échouée sera suivie d’une nouvelle tentative, le nombre de tentatives de tentatives dépend du contexte.
La cohérence est éventuelle, c’est-à-dire que le système n’est plus en état de cohérence lors d’une nouvelle tentative, par exemple si un client a commandé un livre, effectué un paiement puis mis à jour la quantité en stock. Si les opérations de mise à jour du stock échouent et si le dernier stock disponible est le dernier stock disponible, le livre reste disponible jusqu'à la prochaine tentative de mise à jour du stock. Une fois la nouvelle tentative effectuée, votre système sera cohérent.