web-dev-qa-db-fra.com

Comment contourner le manque de transactions dans MongoDB?

Je sais que des questions similaires se posent ici, mais il s’agit soit de me le dire de revenir aux systèmes SGBDR classiques si j’ai besoin de transactions ou d’utiliser opérations atomiques ou validation en deux phases . La deuxième solution semble le meilleur choix. Le troisième que je ne souhaite pas suivre car il semble que beaucoup de choses pourraient mal se passer et je ne peux pas le tester sous tous les aspects. J'ai du mal à refactoriser mon projet pour effectuer des opérations atomiques. Je ne sais pas si cela vient de mon point de vue limité (je n'ai travaillé que jusqu'à présent avec des bases de données SQL) ou s'il est impossible de le faire.

Nous aimerions tester MongoDB dans notre entreprise. Nous avons choisi un projet relativement simple - une passerelle SMS. Il permet à notre logiciel d’envoyer SMS messages au réseau cellulaire et la passerelle fait le sale boulot: communiquer avec les fournisseurs via différents protocoles de communication. La passerelle gère également la facturation des messages. Chaque client qui demande le service doit acheter des crédits. Le système réduit automatiquement le solde de l'utilisateur lorsqu'un message est envoyé et refuse l'accès si le solde est insuffisant. De plus, étant donné que nous sommes clients de fournisseurs tiersSMS, nous pouvons également disposer de nos propres soldes. Nous devons également suivre ces événements.

J'ai commencé à réfléchir à la possibilité de stocker les données requises avec MongoDB si je réduisais un peu la complexité (facturation externe, mise en file d'attente SMS envoi). Venant du monde SQL, je créerais une table séparée pour les utilisateurs, une autre pour les messages SMS, et une autre pour stocker les transactions relatives au solde des utilisateurs. Supposons que je crée des collections séparées pour tous ceux de MongoDB.

Imaginez une tâche d'envoi SMS avec les étapes suivantes de ce système simplifié:

  1. vérifier si l'utilisateur dispose d'un solde suffisant; refuser l'accès s'il n'y a pas assez de crédit

  2. envoyer et stocker le message dans la collection SMS avec les détails et le coût (dans le système en direct, le message aurait un attribut status et une tâche le prendrait pour la livraison et fixerait le prix du SMS en fonction de son état actuel)

  3. diminuer le solde des utilisateurs du coût du message envoyé

  4. enregistrer la transaction dans la collection de transactions

Maintenant, quel est le problème avec ça? MongoDB ne peut effectuer des mises à jour atomiques que sur un seul document. Dans le flux précédent, il pouvait arriver qu'une erreur se produise et que le message soit stocké dans la base de données, mais le solde de l'utilisateur ne soit pas mis à jour et/ou la transaction ne soit pas journalisée.

Je suis venu avec deux idées:

  • Créez une collection unique pour les utilisateurs et stockez le solde en tant que champ, les transactions et les messages liés à l'utilisateur en tant que sous-documents dans le document de l'utilisateur. Comme nous pouvons mettre à jour les documents de manière atomique, cela résout le problème de la transaction. Inconvénients: si l'utilisateur envoie de nombreux SMS messages, la taille du document peut devenir grande et la limite de 4 Mo pouvant être atteinte. Je peux peut-être créer des documents d'historique dans de tels scénarios, mais je ne pense pas que ce serait une bonne idée. Aussi, je ne sais pas à quelle vitesse le système serait si j'appliquais de plus en plus de données dans le même gros document.

  • Créez une collection pour les utilisateurs et une autre pour les transactions. Il peut y avoir deux types de transaction: achat à crédit avec modification du solde positif et messages envoyés avec changement de solde négatif. La transaction peut avoir un sous-document; par exemple dans les messages envoyés , les détails du SMS peuvent être incorporés dans la transaction. Inconvénients: je ne stocke pas le solde utilisateur actuel, je dois donc le calculer chaque fois qu'un utilisateur essaie d'envoyer un message pour indiquer si le message peut être acheminé ou non. Je crains que ce calcul ne soit devenu lent à mesure que le nombre de transactions stockées augmente.

Je suis un peu confus quant à la méthode à choisir. Y a-t-il d'autres solutions? Je ne pouvais trouver aucune meilleure pratique en ligne sur la manière de résoudre ce type de problèmes. Je suppose que beaucoup de programmeurs qui essaient de se familiariser avec le monde NoSQL sont confrontés à des problèmes similaires au début.

135
NagyI

À partir de la version 4.0, MongoDB effectuera des transactions ACID multi-documents. Le plan consiste à activer d'abord les déploiements de jeux de réplicas, suivis des clusters fragmentés. Les transactions dans MongoDB donneront l’impression que les transactions sont familières aux développeurs depuis les bases de données relationnelles: elles sont multi-instructions, avec une sémantique et une syntaxe similaires (comme start_transaction et commit_transaction). Il est important de noter que les modifications apportées à MongoDB qui activent les transactions n’affectent pas les performances pour les charges de travail qui ne les nécessitent pas.

Pour plus de détails, voir here .

Avoir des transactions distribuées ne signifie pas que vous devriez modéliser vos données comme dans des bases de données relationnelles tabulaires. Embrasser la puissance du modèle de document et suivre les bonnes et recommandées pratiques de la modélisation de données.

16
Grigori Melnik

Vivre sans transactions

Support des transactions ACIDE propriétés mais bien qu'il n'y ait pas de transaction dans MongoDB, nous avons des opérations atomiques. Les opérations atomiques signifient que lorsque vous travaillez sur un document unique, ce travail est terminé avant que quiconque ne voie le document. Ils verront tous les changements que nous avons apportés ou aucun d'entre eux. Et en utilisant des opérations atomiques, vous pouvez souvent réaliser la même chose que nous aurions accompli en utilisant des transactions dans une base de données relationnelle. Et la raison en est que, dans une base de données relationnelle, nous devons apporter des modifications à plusieurs tables. Habituellement, les tables doivent être jointes et nous souhaitons le faire tous en même temps. Et pour le faire, comme il y a plusieurs tables, nous devrons commencer une transaction et faire toutes ces mises à jour, puis mettre fin à la transaction. Mais avec MongoDB, nous allons incorporer les données, puisque nous allons le pré-rejoindre dans les documents et ils Sont ces documents riches qui ont une hiérarchie. Nous pouvons souvent accomplir la même chose. Par exemple, dans l'exemple de blog, si nous voulons nous assurer de mettre à jour un article de blog de manière atomique, nous pouvons le faire, car nous pouvons mettre à jour l'intégralité du message de blog en une fois. Où, comme s’il s’agissait d’un ensemble de tables relationnelles, nous devrions probablement ouvrir une transaction pour pouvoir mettre à jour la collection de publications et la collection de commentaires.

Alors, quelles sont nos approches que nous pouvons adopter MongoDB pour pallier le manque de transactions?

  • restructurer - restructurer le code afin que nous travaillions dans un seul document et tirions parti des opérations atomiques que nous proposons dans ce document. Et si nous faisons cela, alors généralement nous sommes tous prêts.
  • implémenter dans le logiciel - nous pouvons implémenter le verrouillage dans le logiciel, en créant une section critique. Nous pouvons créer un test, tester et définir en utilisant find et modify. Nous pouvons construire des sémaphores, si nécessaire. Et d’une certaine manière, c’est ainsi que fonctionne le monde dans son ensemble. Si nous y réfléchissons, si une banque doit transférer de l'argent à une autre banque, elle ne vit pas dans le même système relationnel. Et ils ont souvent leurs propres bases de données relationnelles. Et ils doivent être capables de coordonner cette opération même si nous ne pouvons pas commencer une transaction et mettre fin à une transaction sur ces systèmes de base de données, mais uniquement au sein d'un système dans une banque. Il existe donc certainement des solutions logicielles permettant de contourner le problème.
  • tolère - la dernière approche, qui fonctionne souvent dans les applications Web modernes et les autres applications qui absorbent une quantité considérable de données, consiste simplement à tolérer un peu d'incohérence . Par exemple, si nous parlons d'un fil d'amis dans Facebook, peu importe si tout le monde voit votre mise à jour murale simultanément. Si tout va bien, si une personne a quelques battements de retard pendant quelques secondes et qu'elle se rattrape. Dans de nombreuses conceptions de systèmes, il n'est souvent pas essentiel que tout soit parfaitement cohérent et que tout le monde ait une vue parfaitement cohérente et identique de la base de données. Nous pourrions donc simplement tolérer un peu d'incohérence quelque peu temporaire.

Update, findAndModify, $addToSet (dans une mise à jour) & $Push (dans une mise à jour), les opérations fonctionnent de manière atomique dans un seul document.

81
student

Vérifiez this out, par Tokutek. Ils développent pour Mongo un plugin qui promet non seulement des transactions, mais également une amélioration des performances.

24
Giovanni Bitliner

En résumé: si l'intégrité transactionnelle est un doit, n'utilisez pas MongoDB, mais utilisez uniquement des composants du système prenant en charge les transactions. Il est extrêmement difficile de créer quelque chose sur le composant afin de fournir une fonctionnalité similaire à ACID pour les composants non compatibles ACID. Selon les cas d'utilisation individuels, il peut être judicieux de séparer les actions en actions transactionnelles et non transactionnelles d'une manière ou d'une autre ...

11
Andreas Jung

Maintenant, quel est le problème avec ça? MongoDB ne peut effectuer des mises à jour atomiques que sur un seul document. Dans le flux précédent, il pouvait arriver qu'une erreur se produise et que le message soit stocké dans la base de données, mais que le solde de l'utilisateur ne soit pas réduit et/ou que la transaction ne soit pas journalisée.

Ce n'est pas vraiment un problème. L'erreur que vous avez mentionnée est une erreur logique (bogue) ou IO (réseau, défaillance du disque). Ce type d'erreur peut laisser les magasins sans transaction et les magasins transactionnels dans un état non cohérent. Par exemple, si elle a déjà envoyé SMS mais en enregistrant une erreur de message, elle ne peut pas revenir SMS envoi, ce qui signifie qu'il ne sera pas enregistré, solde de l'utilisateur ne sera pas réduit etc.

Le vrai problème ici est que l’utilisateur peut tirer parti de la situation critique et envoyer plus de messages que son solde ne le permet. Ceci s’applique également au SGBDR, à moins que vous ne fassiez SMS) l’envoi d’une transaction interne avec verrouillage du champ solde (ce qui constituerait un goulot d’étranglement important). Une solution possible pour MongoDB serait d’utiliser findAndModify Tout d’abord, pour réduire le solde et le vérifier, si le résultat est négatif, interdire l’envoi et rembourser le montant (incrément atomique). Si le résultat est positif, poursuivez l’envoi et en cas de défaillance du remboursement. La collecte de l’historique du solde peut également être conservée pour vous aider à réparer/vérifier champ d'équilibre.

7
pingw33n

Les transactions sont absentes dans MongoDB pour des raisons valables. C'est l'une de ces choses qui font que MongoDB est plus rapide.

Dans votre cas, si la transaction est un must, le mongo ne semble pas être un bon choix.

Peut-être RDMBS + MongoDB, mais cela va ajouter des complexités et rendre plus difficile la gestion et le support des applications.

6
kheya

C’est probablement le meilleur blog que j’ai trouvé concernant l’implémentation de transaction comme fonctionnalité pour mongodb.!

Indicateur de synchronisation: idéal pour copier des données à partir d'un document maître

File d'attente: très généraliste, résout 95% des cas. De toute façon, la plupart des systèmes ont besoin d'au moins une file d'attente!

Engagement en deux phases: cette technique garantit que chaque entité dispose toujours de toutes les informations nécessaires pour obtenir un état cohérent.

Log Reconciliation: la technique la plus robuste, idéale pour les systèmes financiers

Gestion des versions: fournit une isolation et prend en charge des structures complexes

Lisez ceci pour plus d'informations: https://dzone.com/articles/how-implement-robust-and

6
Vaibhav

Le projet est simple, mais vous devez prendre en charge les transactions pour le paiement, ce qui rend la chose difficile. Ainsi, par exemple, un système de portail complexe avec des centaines de collections (forum, discussion en ligne, annonces, etc.) est à certains égards plus simple, car si vous perdez un forum ou une entrée de discussion, personne ne s'en soucie vraiment. Par contre, si vous perdez une opération de paiement, le problème est grave.

Donc, si vous voulez vraiment un projet pilote pour MongoDB, choisissez-en un qui est simple that respect.

6
Karoly Horvath

C’est tard, mais pensez que cela aidera à l’avenir. J'utilise Redis pour faire un file d'attente pour résoudre ce problème.

  • Condition:
    L'image ci-dessous montre que 2 actions doivent être exécutées simultanément, mais les phases 2 et 3 de l'action 1 doivent être terminées avant le début de la phase 2 de l'action 2 ou opposée (une phase peut être une requête REST api, demande de base de données ou exécution de code javascript ...). enter image description here

  • Comment une file d'attente vous aide
    File d'attente assurez-vous que chaque code de bloc compris entre lock() et release() dans de nombreuses fonctions ne s'exécutera pas à la même heure; isolez-les.

    function action1() {
      phase1();
      queue.lock("action_domain");
      phase2();
      phase3();
      queue.release("action_domain");
    }
    
    function action2() {
      phase1();
      queue.lock("action_domain");
      phase2();
      queue.release("action_domain");
    }
    
  • Comment créer une file d'attente
    Je ne me concentrerai que sur la manière d'éviter condition de course lors de la création d'une file d'attente sur le site principal. Si vous ne connaissez pas l’idée de base de la file d’attente, venez ici .
    Le code ci-dessous ne montre que le concept, vous devez le mettre en œuvre correctement.

    function lock() {
      if(isRunning()) {
        addIsolateCodeToQueue(); //use callback, delegate, function pointer... depend on your language
      } else {
        setStateToRunning();
        pickOneAndExecute();
      }
    }
    
    function release() {
      setStateToRelease();
      pickOneAndExecute();
    }
    

Mais vous avez besoin de isRunning()setStateToRelease()setStateToRunning() isolez-la ou sinon vous faites face à une situation de concurrence critique. Pour ce faire, je choisis Redis pour ACIDE objectif et évolutif.
Redis document parlez de sa transaction:

Toutes les commandes d'une transaction sont sérialisées et exécutées séquentiellement. Il ne peut jamais arriver qu'une requête émise par un autre client soit servie au cours de l'exécution d'une transaction Redis. Cela garantit que les commandes sont exécutées comme une seule opération isolée.

P/s:
J'utilise Redis parce que mon service l'utilise déjà, vous pouvez utiliser n'importe quel autre moyen de support d'isolation pour le faire.
Le action_domain Dans mon code est supérieur à lorsque vous n'avez besoin que de l'action 1, appel de l'utilisateur A Bloquer l'action 2 de l'utilisateur A, ne bloquez pas les autres utilisateurs. L'idée est de mettre une clé unique pour verrouiller chaque utilisateur.

4
Đinh Anh Huy

Les transactions sont maintenant disponibles dans MongoDB 4.0. Échantillon ici

// Runs the txnFunc and retries if TransientTransactionError encountered

function runTransactionWithRetry(txnFunc, session) {
    while (true) {
        try {
            txnFunc(session);  // performs transaction
            break;
        } catch (error) {
            // If transient error, retry the whole transaction
            if ( error.hasOwnProperty("errorLabels") && error.errorLabels.includes("TransientTransactionError")  ) {
                print("TransientTransactionError, retrying transaction ...");
                continue;
            } else {
                throw error;
            }
        }
    }
}

// Retries commit if UnknownTransactionCommitResult encountered

function commitWithRetry(session) {
    while (true) {
        try {
            session.commitTransaction(); // Uses write concern set at transaction start.
            print("Transaction committed.");
            break;
        } catch (error) {
            // Can retry commit
            if (error.hasOwnProperty("errorLabels") && error.errorLabels.includes("UnknownTransactionCommitResult") ) {
                print("UnknownTransactionCommitResult, retrying commit operation ...");
                continue;
            } else {
                print("Error during commit ...");
                throw error;
            }
       }
    }
}

// Updates two collections in a transactions

function updateEmployeeInfo(session) {
    employeesCollection = session.getDatabase("hr").employees;
    eventsCollection = session.getDatabase("reporting").events;

    session.startTransaction( { readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } } );

    try{
        employeesCollection.updateOne( { employee: 3 }, { $set: { status: "Inactive" } } );
        eventsCollection.insertOne( { employee: 3, status: { new: "Inactive", old: "Active" } } );
    } catch (error) {
        print("Caught exception during transaction, aborting.");
        session.abortTransaction();
        throw error;
    }

    commitWithRetry(session);
}

// Start a session.
session = db.getMongo().startSession( { mode: "primary" } );

try{
   runTransactionWithRetry(updateEmployeeInfo, session);
} catch (error) {
   // Do something with error
} finally {
   session.endSession();
}
3
Manish Jain