Modelos y proyecciones Read
Read Model es una tabla/índice/vista especialmente diseñada para lecturas rápidas en un escenario de producto específico. Proyección: proceso que convierte los eventos/cambios de origen en actualizaciones de Read Model (normalmente upsert idempotent). En conjunto con el CQRS, permite descargar el núcleo OLTP y estabilizar las lecturas p95/p99, controlando la «frescura».
Ideas principales:- Denormalizar bajo petición, no un «esquema universal».
- Actualizar de forma incremental e idempotente.
- Control explícito de staleness y orden.
1) Cuándo usar Modelos de lectura (y cuándo - no)
Adecuado:- Lecturas pesadas frecuentes (joins/agregaciones/ordenaciones) con un retraso de actualización válido.
- Dashboards, catálogos, landings, "top n', fides personales, listas de búsqueda.
- Separación de carga: el núcleo de escritura es estricto, el plano de lectura es rápido y escalable.
- Operaciones que requieren invariantes estrictos «por cada registro» (dinero, singularidad). Hay un path fuerte.
2) Contorno arquitectónico (esquema verbal)
1. Origen de cambios: eventos de dominio (origen de eventos) o CDC desde OLTP.
2. Transportador de proyecciones: parser → agregación/desnormalización → idempotent upsert.
3. Read Store: DB/índice optimizado bajo consulta (RDBMS, columna, búsqueda).
4. API/cliente: SELECT/GET rápidos, con atributos «as_of/freshness».
3) Diseño Read Model
Empieza preguntando: ¿qué campos, filtros, ordenaciones, paginación, top N?
Denormalice: almacene los datos ya combinados (nombres, sumas, estados).
- Partido: por 'tenant _ id', fecha, región.
- Clave primaria: clave de negocio + baqueta temporal (por ejemplo, '(tenant_id, entity_id)' o '(tenant_id, bucket_minute)').
- Índices: por where/order by frecuente.
- TTL/retoque: para escaparates temporales (por ejemplo, 90 días).
4) Flujo de actualizaciones e idempotencia
Idempotent upsert es la base de la estabilidad de las proyecciones.
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);
Reglas:
- Cada mensaje lleva una versión/tiempo; sólo aceptamos «fresco o igual» (idempotency).
- Para agregados (contadores, sumas): almacene el estado y utilice actualizaciones conmutativas (o enfoques CRDT).
5) Fuente de cambios: eventos vs CDC
Eventos (event sourcing): rica semántica, fácil de construir diferentes proyecciones; la evolución de los esquemas es importante.
CDC (replicación lógica): simplemente conecte a una base de datos existente; necesitará DML→sobyty de mapeo y filtración de apdates de ruido.
- Garantías de entrega (at-least-once) y DLQ para mensajes «venenosos».
- Orden por clave (partition key = 'tenant _ id: entity _ id').
6) Orden, causalidad y «frescura»
Orden por clave: los eventos de un solo objeto deben venir en serie; use particiones y versiones.
Causalidad (session/causal): para que el autor vea sus cambios (RYW), pase la versión watermark en las consultas.
Frescura (staleness bounded): devuelva 'as _ of '/' X-Data-Freshness' y mantenga el SLO (por ejemplo, p95 ≤ 60 c).
7) Unidades incrementales y Top N
Ejemplo de backets de venta de minutos: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;
Para el Top N:
- Mantenga el escaparate clasificado (por ejemplo, por 'revenue DESC') y actualice sólo las posiciones cambiantes (heap/skiplist/mesa limitada).
- Almacene la «ventana» del tope (por ejemplo, 100-1000 filas por segmento).
8) Prospecciones y geo-proyecciones
Búsqueda (ES/Opensearch): documento desnormalizado, transformaciones pipelinas, versión del documento = versión de la fuente.
Geo: almacena 'POINT/LAT, LON', preagrega los tailes/cuadrotreles.
9) Multi-tenant y regiones
'tenant _ id' es obligatorio en las claves de proyección y eventos.
Fairness: limite el throughput de las proyecciones per tenant (WFQ/DRR) para que el «ruidoso» no frene al resto.
Residencia: la proyección vive en la misma región que el núcleo de escritura; vitrinas interregionales - agregados/resúmenes.
10) Observabilidad y SLO
Métricas:- 'projection _ lag _ ms' (istochnik→vitrina), 'freshness _ age _ ms' (desde el último delta).
- throughput apdates, proporción de errores, DLQ-rate, redrive-success.
- Tamaño de las vitrinas, p95/p99 latencia lecturas.
- Теги: `tenant_id`, `entity_id`, `event_id`, `version`, `projection_name`, `attempt`.
- Anotaciones: soluciones merge, omitir versiones obsoletas.
11) Playbooks (runbooks)
1. Crecimiento de la laguna: compruebe el conector/corredor, aumente los lotes, incluya la priorización de los escaparates clave.
2. Muchos errores de esquema: congelar el redrive, migrar circuitos (backfill), reiniciar con una nueva versión del mapper.
3. DLQ repetidos: reducir el batch, activar el manejador de «sombra», aumentar la idempotencia.
4. Incoherencia del escaparate: realizar vitrinas rebuild desde el registro/fuente por ventana (selectivamente por tenant/partition).
5. Teclas calientes: restringir la competencia de claves, agregar colas locales, llevar la unidad a un escaparate separado.
12) Recuento completo (rebuild) y backfill
Enfoque:- Detener el consumo (o cambiar a una nueva versión del escaparate).
- Volver a calcular en paquetes (por lotes/fechas/tenantes).
- Habilitar el sweet bifásico: primero rellenamos 'read __ v2', luego cambiamos atomicamente el enrutamiento de lectura.
13) Evolución de los esquemas (versionamiento)
'schema _ version' en eventos/documentos.
La proyección sabe leer varias versiones, una migración «sobre la marcha».
Para los grandes cambios, el nuevo escaparate v2 y el tráfico canario.
14) Seguridad y acceso
Herede RLS/ACL de la fuente; no haga que el escaparate sea más amplio en acceso que los datos originales.
Enmascara PII en proyecciones que no sean necesarias para UX/análisis.
Auditoría de redrives/recuentos/revisiones manuales.
15) Plantilla de configuración
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) Errores típicos
«Un escaparate para todos los casos» → apdates pesados y p99 malos.
Falta de idempotencia → dobles/carreras de caballos en las unidades.
Dual-write directamente al escaparate y OLTP → discrepancias.
Cero visibilidad de frescura → conflicto de expectativas con el producto.
Rebuild sin sweet bifásico → «agujeros» en las respuestas.
No hay lotes/índices → aumento de valor y latencia.
17) Recetas rápidas
Catálogo/búsqueda: escaparate documental + upsert incremental, lag ≤ 5-15 c, índices bajo filtros.
Dashboards: baquetas de minuto/hora, unidades 'SUM/COUNT', p95 frescura ≤ 60 c.
Cinta personal: proyección por usuario + causal/RYW para el autor, fallback por caché.
SaaS global: vitrinas regionales, agregados cruzados-regionales; fairness per tenant.
18) Lista de verificación antes de la venta
- El escaparate está diseñado para una consulta específica; hay índices y lotes.
- Se seleccionó el origen de los cambios (eventos/CDC); garantías de entrega y orden de la llave.
- Upsert idempotente con versiones/tiempo; Protección contra eventos «antiguos».
- El SLO de frescura se define y se da en las respuestas ('as _ of/freshness').
- El DLQ y el redrive seguro están configurados; playback en rebuild/backfill.
- Restricciones de la competencia (serie per-key) y fairness per tenant.
- Métricas/errores/latencia, alertas en p95/p99 y crecimiento DLQ.
- Versificación de esquemas y estrategia de migración (v2 + switch).
- Las directivas de acceso/PII son heredadas y verificadas.
Read Models y proyecciones es un acelerador de lectura de ingeniería: se paga un pequeño precio por la «frescura» y la infraestructura de streaming para obtener milisegundos predecibles y descargar el núcleo de las grabaciones. Diseñe los escaparates bajo petición, haga que los apdates sean idempotentes, mida el trago y prometa claramente frescura, y sus API se mantendrán rápidas incluso cuando la carga, los datos y la geografía crezcan.