DDD et CQRS sont la combinaison dorée

Dans votre travail quotidien, avez-vous rencontré les situations suivantes :

  1. En utilisant une interface existante pour le développement commercial, de graves problèmes de performances sont survenus après sa mise en ligne. Le patron l'a interrogé en public : "Pourquoi n'utilisez-vous pas l'interface cache ? Cette interface utilise toutes la base de données. Comment résister à cela !"

  2. En développant une fonction de gestion backend, les retours d'affaires indiquent que les données sont toujours fausses. Après comparaison, il s'avère que le cache est incohérent avec la base de données. Pourquoi utiliser l'interface de cache ? Êtes-vous perdu dans vos pensées ?

  3. Le produit nécessitait l'ajout de nouvelles fonctionnalités à xxx, et le codage, les tests et la mise en ligne se faisaient en une seule fois. Finalement, il a été découvert qu'un autre processus était bloqué, et une exception s'est produite et a dû être annulée !

  4. Dans un scénario à forte concurrence, la base de données est devenue le goulot d'étranglement du système. Les requêtes sans indexation ne peuvent pas le gérer, et la mise à jour avec indexation ne peut pas le gérer. Comment devrions-nous y faire face ?

  5. À mesure que la quantité de données augmente, le système devient de plus en plus lent, en particulier dans les scénarios de requêtes complexes avec gestion en arrière-plan. Les jointures complexes submergent la base de données.

  6. ……

Pourquoi cela arrive-t-il? L'essentiel est toujours que la structure organisationnelle du code est déraisonnable. Nous mélangeons différentes complexités, créant ainsi une plus grande complexité, puis répétons ce processus, tombant sans le savoir dans un énorme vortex de complexité et incapables de s'en sortir.

1. Qu'est-ce que le CQRS ?

CQRS est l'abréviation de Command Query Responsibility Segregation. Une compréhension simple consiste à séparer les opérations « écriture » ​​(commande) et « lecture » ​​(requête). Les étudiants qui réagissent rapidement diront : « N'est-ce pas une technologie avancée ? N'est-ce pas simplement la séparation de la lecture et de l'écriture dans la base de données ?

Oui, la séparation des lectures et des écritures dans la base de données peut être considérée comme une sorte de CQRS, mais la signification de CQRS est bien plus compliquée que cela.

Le CRQS est à la fois une architecture commerciale populaire et une forme de design thinking.

Le cœur de CQRS est le « fractionnement », qui divise le système complexe en deux parties : commande et requête, utilise différents modes pour différents scénarios, sélectionne la technologie la plus appropriée pour mettre en œuvre la meilleure solution et évite que les deux parties n'interfèrent l'une avec l'autre. .

Le but du CQRS est de réduire la complexité de l’ensemble du système, alors quelle est la logique derrière cela ?

Supposons, dans un système :

  1. La commande a une complexité M

  2. La requête a une complexité N

Si le même ensemble de modèles est utilisé pour traiter la commande et la requête, alors dans les cas extrêmes, la complexité du système est M * N, car les deux s'influencent mutuellement, et lors de l'ajustement de l'un, vous devez toujours faire attention à l'impact sur l'autre. .

Ce type de méthode de conception du type « tu m'as et je t'ai », « l'interaction entre les deux » est devenue la partie la plus compliquée du système, et beaucoup d'énergie est dépensée pour « vérifier l'impact » au lieu de conception et codage les plus précieux.

Si Commande et Requête sont complètement séparées, la complexité du système devient M + N. Les modifications apportées à la commande n’affecteront pas la requête, et les modifications apportées à la requête n’affecteront pas la commande.

Bien entendu, les deux extrêmes ci-dessus sont rares dans le travail réel, et la complexité du système se situe généralement entre les deux.

Il ne s'agit là que d'une déduction théorique : les « conflits » que l'on constate partout dans le travail réel sont aussi un soupçon de « clivage ».

2. Conflits dans l'architecture en couches 

L'architecture en couches la plus courante est présentée comme suit :

Comme le montre la figure, le système est divisé en 5 couches, et la signification de chaque couche est la suivante :

  1. Couche d'accès Web. Il est principalement utilisé pour traiter les entrées du système, vérifier les informations saisies, appeler les services d'application pour terminer les opérations commerciales, convertir les résultats et enfin les renvoyer à l'appelant ;

  2. Couche de service d’application. Gère principalement l'orchestration des processus métier, obtient les objets de domaine de l'entrepôt, effectue les opérations commerciales du modèle de domaine, synchronise le dernier statut des objets avec le moteur de stockage de données via l'entrepôt et publie les événements de domaine en externe ;

  3. Couche de domaine. Le point porteur de la logique métier est une expression concentrée de la valeur commerciale.Elle est généralement construite sur une conception orientée objet et garantit la réutilisabilité et l'évolutivité de la logique métier basée sur des fonctionnalités telles que l'encapsulation, l'héritage et le polymorphisme;

  4. Niveau entrepôt. Principalement utilisé pour l'accès aux données, fournissant des services d'exploitation de données pour les services d'application vers le haut et protégeant les différences des différents moteurs de stockage vers le bas ;

  5. couche de données. Principalement utilisé pour le stockage et la récupération de données.Les moteurs de stockage de données courants appartiennent tous à cette couche, comme MySQL, Redis, ES, etc.;

En fait, l'architecture en couches elle-même est également une sorte de « clivage », encapsulant différentes préoccupations à différents niveaux. Mais en plus de la superposition horizontale, il peut également être divisé verticalement en fonction du CQRS, c'est-à-dire que les composants de chaque couche sont divisés en deux parties : Commande et Requête.

Étant donné que le conflit de couche d'accès est faible, le fractionnement lui-même n'a que peu d'importance et n'est pas requis ici. Cependant, au sens strict, le fractionnement est toujours recommandé.

 

3. Conflit et division de la couche de service d'application

La division de la couche de service d'application consiste à diviser un service d'application en deux groupes : CommandService et QueryService.

Cela peut éviter bien des problèmes inutiles. Il existe des différences majeures entre Command et Query, comme suit :

Service de commande Service de requête
Les composants dépendants sont différents Service de vérification ValidateService ; service de chargement différé LazyLoaderFactory ; entrepôt CommandRepository sans mise en cache ; éditeur d'événements EventPublisher Entrepôt QueryRepository avec fonction de mise en cache ; service d'agrégation de données JoinService ; service de conversion de données Converter
Les processus de base sont différents Validation, chargement, opérations métiers, synchronisation, publication d'événements Validation, chargement, assemblage de données, conversion
Différentes fonctions sont améliorées Principalement gestionnaire de transactions Principalement des composants de mise en cache

En repensant au scénario mentionné au début, une fois la division de la couche application terminée, vous n'avez plus à vous soucier de l'utilisation des mauvais composants :

  1. Le référentiel de CommandService n'utilise pas de cache et exploite uniquement la base de données

  2. Le référentiel de QueryService peut utiliser la mise en cache pour améliorer les performances d'accès

De plus, pour un processus opérationnel unifié, une abstraction supplémentaire peut être utilisée pour éliminer les « codes modèles » répétés, tels que :

  1. Présentation du « modèle de conception de méthode modèle » pour parvenir à la réutilisation de la logique de base

    1. Abstraction des deux classes parentes BaseCommandService et BaseQueryService pour unifier le processus principal

    2. Les sous-classes implémentent les méthodes abstraites de BaseCommandService et BaseQueryService pour terminer l'expansion des fonctions

  2. Utilisation du modèle Proxy basé sur la « convention sur la configuration », définissant uniquement les interfaces sans écrire de code d'implémentation

    1. Définir les interfaces CommandService et QueryService selon les spécifications et compléter les configurations pertinentes via des annotations

    2. Générez automatiquement des classes d'implémentation de proxy pour compléter l'orchestration des processus

 

4. Conflit et division des couches de modèles

La couche modèle est au cœur du système et sa conception affecte directement la qualité de l’ensemble du système. Au cœur de la logique métier, la stratégie de mise en œuvre du processus de comparaison comprend :

  1. Le cœur de la conception basée sur le domaine DDD consiste à utiliser des fonctionnalités avancées orientées objet (encapsulation, héritage, polymorphisme, composition, etc.) pour la conception, ce qui est très adapté aux scénarios commerciaux complexes. Sa manifestation est qu'il existe de nombreux groupes d'objets à forte cohésion et à faible couplage (racines agrégées), et la logique métier est complétée par la coopération de ces petits objets ;

  2. Les scripts de transaction utilisent la pensée procédurale pour intégrer les opérations de données dans les processus et conviennent mieux aux scénarios commerciaux simples. Sa manifestation est qu'il existe de nombreux « services divins », et qu'il existe de nombreuses méthodes très longues dans le service, et que la logique métier est complétée par ces méthodes ;

Quant à savoir quelle est la meilleure solution, il y a un débat sur Internet depuis de nombreuses années, et il n'y a finalement pas de conclusion. Mais je pense toujours que « discuter de solutions sans scénarios commerciaux, c'est simplement jouer aux hooligans ».

À partir de différents scénarios d’application, les conclusions suivantes peuvent être tirées :

  1. Les scénarios de commande doivent garantir une logique métier rigoureuse et sont généralement complexes. DDD est donc la solution optimale.

  2. Les scénarios de requête nécessitent des capacités d'assemblage de données plus flexibles et sont généralement relativement simples. Les scripts de transaction constituent donc la solution optimale.

Je dis souvent : « L’écriture » ​​la plus simple est également complexe, et la « lecture » ​​la plus complexe est également simple. » La logique derrière cela est basée sur le jugement de scène de Command et Query.

Divisez le modèle en Commande et Requête, comme suit :

 

Une fois la division du modèle terminée, le nouveau modèle présente les caractéristiques suivantes :

  1. Agg est également la racine agrégée de DDD, qui est principalement utilisée pour traiter une logique de commande complexe et se compose d'« objets riches » avec un grand nombre d'opérations commerciales ;

  2. View est un POJO standard, qui agit principalement comme un objet de résultat de requête. C'est un « objet anémique » typique et ne sert que de support de données, rassemblant les données en fonction des exigences d'affichage ;

  3. View n'a pas son propre référentiel et ne peut s'appuyer que sur CommandRepository pour obtenir des données. Le composant Converter est responsable de la conversion du modèle Agg en modèle View ;

C'est le point clé de la scission. Pour faciliter la compréhension, permettez-moi de vous donner un exemple simple :

Par exemple, dans le module de commande du e-commerce :

  • Processus de génération de commande , avec Order comme racine globale pour coordonner les commandes internes OrderItem et PayInfo.

  • Page de liste de commandes, il suffit d'afficher les informations sur la commande et l'utilisateur

  • Détails de la commande, nécessité d'afficher la commande, l'utilisateur, l'adresse, l'article de commande, les informations de paiement, le produit et d'autres informations

Si un modèle prend en charge trois scénarios en même temps, le modèle lui-même devient très complexe et il est difficile de déterminer à quel scénario appartient une méthode ou un champ donné.

À ce stade, le modèle doit être divisé selon le scénario :

  1. OrderBO est modélisé en mode DDD, fournit des opérations commerciales unifiées en externe et coordonne plusieurs objets d'entité tels que OrderItem et PayInfo en interne ;

  2. OrderListVO est modélisé en mode POJO et ses attributs contiennent des informations sur la commande et l'utilisateur ;

  3. OrderDetailVO est modélisé en mode POJO et ses attributs incluent la commande, l'utilisateur, l'adresse, l'article de commande, PayInfo, le produit et d'autres informations ;

Les trois modèles sont indépendants les uns des autres et ne s’influencent pas mutuellement.

Bien entendu, du fait de l'utilisation d'un Repository unifié, il est également nécessaire de fournir un Convertisseur correspondant à VO :

  1. OrderListVOConverter convertit OrderBO en objet OrderListVO

  2. OrderDetailVOConverter convertit OrderBO en objet OrderDetailVO

 

5. Conflit et division des couches d'entrepôt 

Il est également très nécessaire de diviser la couche entrepôt. Il existe plusieurs conflits principaux au niveau de cette couche :

Référentiel de commandes Référentiel de requêtes
La mise en œuvre sous-jacente est différente Principalement basé sur l'implémentation de DB Basé sur DB, Redis, ES et autres moteurs de stockage
La complexité de la méthode varie Fournit seulement quelques méthodes et est suffisant pour prendre en charge la plupart des scénarios, tels que la sauvegarde, la mise à jour, getById, etc. Personnalisé selon des scénarios métiers, avec diverses méthodes (single, batch, pagination, tri, statistiques, etc.) et différentes dimensions (id, user, status)
La valeur de retour est différente Renvoie directement l'objet riche entièrement assemblé Personnalisez les valeurs de retour en fonction des scénarios commerciaux

La structure globale après la division de l'entrepôt est la suivante :

Le fractionnement d'entrepôt présente les caractéristiques suivantes :

  1. View ne nécessite plus le composant Converter pour terminer la conversion des données

  2. Les données de View proviennent de son propre référentiel et peuvent être personnalisées de manière flexible en fonction des besoins d'affichage.

  3. La commande et la requête utilisent toujours la même base de données et la même table de données

 

6. Conflit et division de la couche de données 

La division de la couche de données est la division la plus importante.En matière de séparation, la première réaction est la « séparation maître-esclave de la base de données ».

L'essence de la division des couches de données est que les meilleurs scénarios d'application des différents moteurs de stockage de données varient considérablement et qu'il existe souvent des contradictions dans l'optimisation de la lecture et de l'écriture.

Prenons toujours comme exemple la base de données la plus courante :

  1. Pour améliorer les performances des requêtes, il est recommandé de créer des index pour différentes dimensions de requête.

  2. Pour améliorer les performances d'écriture, vous devez avoir de moins en moins d'index sur la table

  3. Afin d'accélérer les performances de mise à jour, il est recommandé d'utiliser les trois formes normales pour concevoir la structure du tableau afin de réduire les informations redondantes.

  4. Afin d'accélérer les performances des requêtes, il est recommandé d'utiliser une conception anti-paradigme, d'essayer de redondant les données et d'éviter les opérations de jointure entre les tables de données.

Vous ne pouvez pas avoir à la fois une patte de poisson et une patte d'ours, et cela s'affiche de manière éclatante au niveau de la couche de base de données !

Une fois la couche de données divisée, la structure est la suivante :

Le modèle présente les caractéristiques suivantes :

  1. Le stockage des données a été complètement divisé : Command et Query peuvent choisir de manière flexible le moteur de stockage le plus approprié ;

  2. Command et Query doivent introduire un mécanisme de synchronisation pour terminer la synchronisation des données. Les mécanismes de synchronisation courants sont :

    1. Fonctionne au niveau de la couche application pour synchroniser les données en fonction des événements de domaine, comme le montre la figure

    2. Travaillez dans la couche de données basée sur la synchronisation des données de journal, telle que la synchronisation maître-esclave de MySQL, Canal2XX, etc.

Le fractionnement des couches de données est la destination finale des grands systèmes. Prenons toujours le système de commande comme exemple :

  1. En tant que système avec des exigences de cohérence extrêmement élevées, le premier choix pour les commandes reste une base de données relationnelle avec ACID côté commande. Même s'il s'agit d'une sous-base de données ou d'une sous-table, le stockage sous-jacent reste inchangé ;

  2. Afin de répondre aux exigences de requêtes hautes performances, Redis doit être introduit côté requête en tant que cache distribué pour accélérer l'accès ;

  3. Afin de répondre aux requêtes métier complexes et multidimensionnelles en arrière-plan, ES doit être introduit du côté des requêtes pour accélérer la récupération du texte intégral ;

  4. Afin de répondre aux diverses exigences de reporting en temps réel, TiDB doit être introduit du côté des requêtes pour répondre à la récupération en temps réel de données massives ;

C'est là que nous aboutissons : « Systèmes à forte intensité de données » Un nombre croissant d'applications ont des exigences strictes et larges, et un seul outil ne suffit pas à répondre à tous les besoins de traitement et de stockage des données. Au lieu de cela, le travail global est divisé en une série de tâches qui peuvent être accomplies efficacement par un seul outil, et elles sont assemblées via le code de l'application, et les services sont fournis en externe via des API, protégeant ainsi la complexité interne.

 

7. Méthode d'amélioration des performances des bases de données relationnelles

L'amélioration des performances des bases de données relationnelles est l'un des défis importants auxquels sont confrontés de nombreuses grandes applications et sites Web. Voici deux méthodes courantes pour améliorer les performances des bases de données relationnelles :

Séparation lecture-écriture

  • Concept : La séparation en lecture et en écriture est une stratégie qui sépare les opérations de lecture et les opérations d'écriture dans la base de données. En règle générale, les applications effectuent beaucoup plus d'opérations de lecture que d'opérations d'écriture. Les opérations de lecture peuvent donc être affectées à un ou plusieurs serveurs de base de données qui sont uniquement responsables de la lecture des données, tandis que les opérations d'écriture sont envoyées au serveur de base de données principal.

  • Flux de travail :

    • Base de données principale (base de données d'écriture) : responsable de la gestion des opérations d'écriture, y compris l'insertion, la mise à jour et la suppression de données.
    • Base de données esclave (base de données de lecture) : responsable du traitement des opérations de lecture, y compris l'interrogation des données. La base de données esclave est généralement une copie de la base de données maître pour garantir la cohérence des données.
  • Avantages :

    • Améliore les performances des opérations de lecture car elles peuvent être traitées en parallèle.
    • Réduisez la charge sur la base de données principale afin qu'elle puisse se concentrer davantage sur les opérations d'écriture.
    • Améliorez l'évolutivité de la base de données en ajoutant davantage de bases de données esclaves pour prendre en charge davantage d'opérations de lecture.
  • Choses à noter :

    • La copie des données peut entraîner une certaine latence, de sorte que les lectures peuvent ne pas refléter immédiatement les dernières écritures.
    • Des stratégies de cohérence et de synchronisation des données doivent être prises en compte.

Partage

  • Concept : le partitionnement de base de données et le partitionnement de tables sont une stratégie consistant à diviser une base de données en plusieurs sous-bases de données ou tables de données indépendantes. Chaque sous-base de données ou table est responsable du stockage des données dans une plage ou une condition spécifique. De cette manière, les requêtes et les écritures peuvent être distribuées sur différents nœuds de base de données, réduisant ainsi la charge sur une seule base de données.

  • Flux de travail :

    • Partage horizontal des tables de base de données : distribuez les données dans différentes tables en fonction d'un certain champ de données (par exemple, l'ID utilisateur ou l'horodatage).
    • Partage horizontal de la base de données : distribuez les données dans différentes bases de données en fonction d'un certain attribut des données (par exemple, l'emplacement géographique ou le type d'entreprise).
  • Avantages :

    • Améliore l'évolutivité de la base de données car les données peuvent être distribuées sur plusieurs nœuds.
    • Réduisez la charge sur une seule base de données et améliorez les performances.
    • Rend la base de données plus facile à maintenir et à sauvegarder car les données sont stockées de manière décentralisée.
  • Choses à noter :

    • Une stratégie de partitionnement bien conçue est nécessaire pour garantir une distribution uniforme des données.
    • Lorsque des requêtes doivent être exécutées sur plusieurs fragments, des plans de requête complexes peuvent être nécessaires.

Ces méthodes peuvent être utilisées individuellement ou en combinaison, choisies en fonction des besoins et des exigences de performances de l'application. Par exemple, un grand site Web de commerce électronique peut utiliser la séparation lecture-écriture pour améliorer les performances de recherche de produits, et utiliser le partitionnement et le partitionnement pour gérer le stockage et la récupération des données de commande. Considérées ensemble, ces stratégies peuvent aider à résoudre différents niveaux de problèmes de performances des bases de données.

 

8. Résumé

La « division » est l'un des moyens importants de « séparer les préoccupations ». Le but du fractionnement est de classer les problèmes puis de prendre des mesures ciblées pour mieux les résoudre.

En tant qu'architecture, CQRS classe différentes parties du système métier. Ensuite, nous devons trouver la solution optimale pour Command and Query :

  1. Le commandement utilise le DDD comme base théorique pour mettre en œuvre le meilleur combat pratique dans le modèle tactique, notamment

    1. conception globale

    2. conception d'entrepôt

    3. LazyLoad + Mode contextuel

    4. Vérification commerciale

    5. événements de domaine

  2. Query prend la récupération et l'assemblage de données comme ses principales capacités. La conception est laissée aux développeurs et la mise en œuvre est laissée au framework, y compris

    1. Mode objet de requête QueryObject

    2. Mode de connexion à la mémoire

    3. Mode table large et table redondante

 

 

Je suppose que tu aimes

Origine blog.csdn.net/summer_fish/article/details/132777065
conseillé
Classement