Modèles de lecture et projections
Read Model est une table/index/vue spécialement conçue pour des lectures rapides sous un scénario de produit spécifique. Projection - processus qui convertit les événements/changements de source en mises à jour du modèle de lecture (généralement idempotent upsert). Associé au CQRS, il permet de décharger le noyau OLTP et de stabiliser les lectures p95/p99 en contrôlant la « fraîcheur ».
Idées principales :- Dénormaliser sous la demande, pas le « schéma universel ».
- Mise à jour incrémentale et idempotente.
- Gérer clairement la staleness et l'ordre.
1) Quand utiliser les modèles de lecture (et quand - pas)
Convient :- Lectures fréquentes et lourdes (joines/agrégations/tri) avec un délai de mise à jour acceptable.
- Dashboards, catalogues, lendings, "top N', fides personnelles, listes de recherche.
- Partage de charge : write-core - rigoureux, read-plan - rapide et évolutif.
- Opérations nécessitant des invariants stricts « pour chaque enregistrement » (argent, unicité). C'est un strong path.
2) Contour architectural (schéma verbal)
1. Source des changements : événements de domaine (event sourcing) ou CDC de OLTP.
2. Convoyeur de projection : parser → aggrégation/dénormalisation → idempotent upsert.
3. Read Store : Bases de données/index optimisées pour la demande (RDBMS, colonnes, moteurs de recherche).
4. API/client : SELECT/GET rapide, avec les attributs « as_of/freshness ».
3) Conception du modèle de lecture
Commencez par demander : quels champs, filtres, triage, pagination, top N ?
Dénormaliser : stocker les données déjà combinées (titres, montants, statuts).
- Lot : par 'tenant _ id', date, région.
- Clé primaire : clé d'entreprise + baquet temporaire (par exemple, '(tenant_id, entity_id)' ou '(tenant_id, bucket_minute)').
- Index : par where/order fréquents par.
- TTL/rétention : pour les vitrines temporaires (par exemple 90 jours).
4) Flux de mises à jour et idempotence
Idempotent upsert est la base de la stabilité des projections.
Pseudo :sql
-- Projection table
CREATE TABLE read_orders (
tenant_id TEXT,
order_id UUID,
status TEXT,
total NUMERIC(12,2),
customer JSONB,
updated_at TIMESTAMP,
PRIMARY KEY (tenant_id, order_id)
);
-- Idempotent update by event
INSERT INTO read_orders(tenant_id, order_id, status, total, customer, updated_at)
VALUES (:tenant,:id,:status,:total,:customer,:ts)
ON CONFLICT (tenant_id, order_id) DO UPDATE
SET status = EXCLUDED. status,
total = EXCLUDED. total,
customer = COALESCE(EXCLUDED. customer, read_orders. customer),
updated_at = GREATEST(EXCLUDED. updated_at, read_orders. updated_at);
Règles :
- Chaque message porte une version/heure ; nous ne prenons que « frais ou égal » (idempotency).
- Pour les agrégats (compteurs, montants) - Stockez l'état et utilisez des mises à jour commutatives (ou des approches CRDT).
5) Source du changement : événements vs CDC
Événements (event sourcing) : riche sémantique, facile à construire différentes projections ; l'évolution des schémas est importante.
CDC (réplication logique) : se connecter simplement à une base de données existante ; le mappage des DML→sobyty et le filtrage des updates sonores seront nécessaires.
- Garanties de livraison (at-least-once) et DLQ pour les messages « toxiques ».
- Ordre par clé (partition key = 'tenant _ id : entity _ id').
6) Ordre, causalité et « fraîcheur »
Ordre par clé : les événements d'un objet doivent venir successivement ; utilisez le lot et les versions.
Causalité (session/causal) : pour que l'auteur voit ses changements (RYW), passez la version watermark dans les requêtes.
Fraîcheur (staleness bounded) : retournez 'as _ of '/' X-Data-Freshness'et tenez le SLO (p.ex. p95 ≤ 60c).
7) Agrégats incrémentiels et top N
Exemple de bacs de vente minutes :sql
CREATE TABLE read_sales_minute (
tenant_id TEXT,
bucket TIMESTAMP, -- toStartOfMinute revenue NUMERIC(14,2),
orders INT,
PRIMARY KEY (tenant_id, bucket)
);
-- Update by Event
INSERT INTO read_sales_minute(tenant_id, bucket, revenue, orders)
VALUES (:tenant,:bucket,:amount, 1)
ON CONFLICT (tenant_id, bucket) DO UPDATE
SET revenue = read_sales_minute. revenue + EXCLUDED. revenue,
orders = read_sales_minute. orders + 1;
Pour le top N :
- Maintenez la vitrine classée (par exemple, « revenue DESC ») et ne mettez à jour que les positions modifiées (heap/skiplist/limited table).
- Stockez la « fenêtre » du haut (par exemple, 100-1000 lignes par segment).
8) Recherches et géo-projections
Recherche (ES/Opensearch) : Document dénormalisé, pipeline de transformation, version du document = version source.
Géo : stockez 'POINT/LAT, LON', pré-agrégez les mystères/quadrotri.
9) Multi-tenants et régions
'tenant _ id' est obligatoire dans les clés des projections et des événements.
Fairness : limitez les projections per tenant (WFQ/DRR) afin que le « bruyant » ne freine pas les autres.
Résidence : la projection vit dans la même région que le noyau d'écriture ; vitrines interrégionales - agrégats/résumés.
10) Observabilité et SLO
Métriques :- « projection _ lag _ ms' (istochnik→vitrina), » freshness _ age _ ms' (depuis le dernier delta).
- throughput updates, proportion d'erreurs, DLQ-rate, redrive-success.
- Taille des vitrines, p95/p99 latence des lectures.
- Теги: `tenant_id`, `entity_id`, `event_id`, `version`, `projection_name`, `attempt`.
- Annotations : solutions merge, ignorer les versions obsolètes.
11) Pleybooks (runbooks)
1. Croissance de la lagune : vérifier le connecteur/courtier, augmenter les lots, inclure la hiérarchisation des vitrines clés.
2. Beaucoup d'erreurs de schéma : geler la redrive, migrer les schémas (backfill), redémarrer avec la nouvelle version du mupper.
3. DLQ répétées : réduire le batch, activer le gestionnaire « shadow », augmenter l'idempotence.
4. Incohérence de la vitrine : effectuer des rebuild vitrines à partir du journal/source par fenêtre (sélectivement par tenant/partition).
5. Clés chaudes : limiter la concurrence par clé, ajouter des files d'attente locales, sortir l'unité dans une vitrine distincte.
12) Total recalculing (rebuild) et backfill
Approche :- Arrêter la consommation (ou passer à une nouvelle version de la vitrine).
- Recalculer par lots (par lots/dates/tenants).
- Activer le pull biphasé : d'abord remplir 'read __ v2', puis commuter atomiquement le routage des lectures.
13) Évolution des schémas (versioning)
'schema _ version 'dans les événements/documents.
La projection peut lire plusieurs versions, la migration à la volée.
Pour les changements majeurs - la nouvelle vitrine v2 et le trafic canarien.
14) Sécurité et accès
Hériter du RLS/ACL de la source ; ne faites pas une vitrine plus large d'accès que les données initiales.
Masquer le PII dans des projections inutiles pour UX/Analysis.
Vérification des révisions/recalculations/modifications manuelles.
15) Modèle de configuration
yaml projections:
read_orders:
source: kafka. orders. events partition_key: "{tenant_id}:{order_id}"
idempotency: version_ts upsert:
table: read_orders conflict_keys: [tenant_id, order_id]
freshness_slo_ms: 60000 dlq:
topic: orders. events. dlq redrive:
batch: 500 rate_limit_per_sec: 50 read_sales_minute:
source: cdc. orders partition_key: "{tenant_id}:{bucket_minute}"
aggregate: increment retention_days: 90 limits:
per_tenant_parallelism: 4 per_key_serial: true observability:
metrics: [projection_lag_ms, dlq_rate, redrive_success, read_p95_ms]
16) Erreurs typiques
« Une vitrine pour tous les cas » → des apdées lourdes et de mauvaises p99.
Manque d'idempotence → prises/sauts dans les unités.
Dual-write directement dans la vitrine et OLTP → divergence.
Zéro visibilité de fraîcheur → conflit d'attentes avec le produit.
Rebuild sans pull en deux phases → « trous » dans les réponses.
Pas de lots/indices → augmentation de la valeur et de la latence.
17) Recettes rapides
Catalogue/recherche : vitrine documentaire + upsert incrémental, lag ≤ 5-15 c, index sous filtres.
Dashboards : baquets minutes/heures, agrégats 'SUM/COUNT', p95 fraîcheur ≤ 60 c.
Bande personnelle : projection par utilisateur + causal/RYW pour l'auteur, fallback par cache.
Global SaaS : vitrines régionales, agrégats transrégionaux ; fairness per tenant.
18) Chèque-liste avant la vente
- La vitrine est conçue pour répondre à une demande spécifique ; il y a des indices et des lots.
- Source du changement choisie (événements/CDC) ; garanties de livraison et ordre par clé.
- Idempotent upsert avec versions/temps ; protection contre les « anciens » événements.
- Le SLO de fraîcheur est défini et donné dans les réponses (« as _ of/freshness »).
- DLQ et redrive sécurisé sont personnalisés ; pleybuk sur rebuild/backfill.
- Restrictions de concurrence (série per-key) et fairness per tenant.
- Métriques de laga/error/latency, alertes à p95/p99 et croissance du DLQ.
- Versionation des schémas et stratégie de migration (v2 + pull).
- Les politiques d'accès/PII sont héritées et validées.
Conclusion
Les modèles et projections de lecture sont un accélérateur de lecture d'ingénierie : vous payez un petit prix de « fraîcheur » et d'infrastructure de streaming pour obtenir des millisecondes prévisibles et décharger le noyau des enregistrements. Concevez des vitrines à la demande, faites des apdées idempotentes, mesurez la larme et promettez clairement de la fraîcheur - et vos API resteront rapides même si la charge, les données et la géographie augmentent.