GH GambleHub

Read Models і проекції

Read Model - це спеціально спроектована таблиця/індекс/вид для швидких читань під конкретний продуктовий сценарій. Проекція - процес, який перетворює події/зміни джерела в оновлення Read Model (зазвичай idempotent upsert). У зв'язці з CQRS це дозволяє розвантажити OLTP-ядро і стабілізувати p95/p99 читань, контролюючи «свіжість».

Головні ідеї:
  • Денормалізувати під запит, а не «універсальну схему».
  • Оновлювати інкрементально та ідемпотентно.
  • Явно управляти staleness і порядком.

1) Коли використовувати Read Models (і коли - ні)

Підходить:
  • Часті важкі читання (джоїни/агрегації/сортування) з допустимою затримкою оновлення.
  • Дашборди, каталоги, лендінги, «топ-N», персональні фіди, пошукові списки.
  • Розділення навантаження: write-ядро - суворе, read-площина - швидка і масштабована.
Не підходить:
  • Операції, що вимагають строгих інваріантів «на кожен запис» (гроші, унікальності). Там - strong path.

2) Архітектурний контур (словесна схема)

1. Джерело змін: події домену (event sourcing) або CDC з OLTP.
2. Конвеєр проекцій: парсер → аггрегація/денормалізація → idempotent upsert.
3. Read Store: БД/індекс, оптимізовані під запит (RDBMS, колонічні, пошукові).
4. API/клієнт: швидкі SELECT/GET, з атрибутами «as_of/freshness».

3) Проектування Read Model

Почніть із запиту: які поля, фільтри, сортування, пагінація, топ-N?
Денормалізуйте: зберігайте вже об'єднані дані (назви, суми, статуси).

Ключі:
  • Партіонування: по'tenant _ id', даті, регіоні.
  • Primary key: бізнес-ключ + часовий бакет (наприклад, «(tenant_id, entity_id)» або «(tenant_id, bucket_minute)»).
  • Індекси: за частими where/order by.
  • TTL/ретеншн: для тимчасових вітрин (наприклад, 90 днів).

4) Потік оновлень та ідемпотентність

Idempotent upsert - основа стабільності проекцій.

Псевдо:
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);
Правила:
  • Кожне повідомлення несе версію/час; приймаємо тільки «свіже або рівне» (idempotency).
  • Для агрегатів (лічильники, суми) - зберігайте state і використовуйте комутативні оновлення (або CRDT-підходи).

5) Джерело змін: події vs CDC

Події (event sourcing): багата семантика, легко будувати різні проекції; важлива еволюція схем.
CDC (логічна реплікація): просто підключити до існуючої БД; буде потрібно маппінг DML→sobyty і фільтрація шумових апдейтів.

Обидва варіанти вимагають:
  • Гарантій доставки (at-least-once) і DLQ для «отруйних» повідомлень.
  • Порядку по ключу (partition key ='tenant _ id:entity_id`).

6) Порядок, причинність і «свіжість»

Порядок за ключем: події одного об'єкта повинні приходити послідовно; використовуйте партіонування і версії.
Причинність (session/causal): щоб автор бачив свої зміни (RYW), передавайте watermark версії в запитах.
Свіжість (bounded staleness): повертайте'as _ of '/' X-Data-Freshness'і тримайте SLO (наприклад, p95 ≤ 60 c).

7) Інкрементальні агрегати і топ-N

Приклад хвилинних бакетів продажів:
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;
Для топ-N:
  • Підтримуйте ранжовану вітрину (наприклад, по'revenue DESC') і оновлюйте тільки змінені позиції (heap/skiplist/limited table).
  • Зберігайте «вікно» топа (наприклад, 100-1000 рядків на сегмент).

8) Пошукові та гео-проекції

Пошук (ES/Opensearch): денормалізований документ, pipeline трансформацій, версія документа = версія джерела.
Гео: зберігайте'POINT/LAT, LON', попередньо агрегуйте тайли/квадротрі.

9) Мульти-тенант і регіони

'tenant _ id'обов'язковий в ключах проекцій і подіях.
Fairness: лімітуйте throughput проекцій per tenant (WFQ/DRR), щоб «галасливий» не гальмував інших.
Residency: проекція живе в тому ж регіоні, що і write-ядро; міжрегіональні вітрини - агрегати/зведення.

10) Спостережуваність і SLO

Метрики:
  • 'projection _ lag _ ms'( istochnik→vitrina),'freshness _ age _ ms'( з моменту останньої дельти).
  • throughput апдейтів, частка помилок, DLQ-rate, redrive-success.
  • Розмір вітрин, p95/p99 латентність читань.
Трейсинг/логи:
  • Теги: `tenant_id`, `entity_id`, `event_id`, `version`, `projection_name`, `attempt`.
  • Анотації: merge-рішення, пропуски застарілих версій.

11) Плейбуки (runbooks)

1. Зростання лага: перевірити конектор/брокер, збільшити партії, включити пріоритизацію ключових вітрин.
2. Багато помилок схеми: заморозити редрайв, виконати міграцію схем (backfill), перезапустити з новою версією маппера.
3. Повторні DLQ: зменшити batch, включити «тіньовий» обробник, посилити ідемпотентність.
4. Неузгодженість вітрини: виконати rebuild вітрини з журналу/джерела за вікно (селективно по tenant/partition).
5. Гарячі ключі: обмежити конкуренцію по ключу, додати локальні черги, винести агрегат в окрему вітрину.

12) Повний перерахунок (rebuild) і backfill

Підхід:
  • Зупинити споживання (або перемкнути на нову версію вітрини).
  • Перерахувати пакетами (за партіями/датами/тенантами).
  • Увімкнути двофазний світч: спочатку заповнюємо'read __ v2', потім атомарно перемикаємо маршрутизацію читань.

13) Еволюція схем (версіонування)

'schema _ version'у подіях/документах.
Проекція вміє читати кілька версій, міграція «на льоту».
Для великих змін - нова вітрина v2 і канарний трафік.

14) Безпека та доступ

Успадкуйте RLS/ACL від джерела; не робіть вітрину ширше по доступу, ніж вихідні дані.
Маскуйте PII в проекціях, не потрібних для UX/аналітики.
Аудит редрайвів/перерахунків/ручних правок.

15) Конфігураційний шаблон

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) Типові помилки

«Одна вітрина на всі випадки» → важкі апдейти і погані p99.
Відсутність ідемпотентності → дублі/стрибки в агрегатах.
Dual-write безпосередньо в вітрину і OLTP → розбіжності.
Нульова видимість свіжості → конфлікт очікувань з продуктом.
Rebuild без двофазного світчу → «дірки» у відповідях.
Немає партіонування/індексів → зростання вартості і латентності.

17) Швидкі рецепти

Каталог/пошук: документна вітрина + інкрементальний upsert, lag ≤ 5-15 c, індекси під фільтри.
Дашборди: хвилинні/годинні бакети, агрегати'SUM/COUNT', p95 свіжості ≤ 60 c.
Персональна стрічка: проекція по користувачеві + causal/RYW для автора, fallback на кеш.
Глобальний SaaS: регіональні вітрини, агрегати крос-регіонально; fairness per tenant.

18) Чек-лист перед продом

  • Вітрина спроектована під конкретний запит; є індекси і партії.
  • Джерело змін вибрано (події/CDC); гарантії доставки і порядок за ключем.
  • Ідемпотентний upsert з версіями/часом; захист від «старих» подій.
  • SLO свіжості визначений і віддається у відповідях ('as _ of/freshness').
  • DLQ і безпечний редрайв налаштовані; плейбук на rebuild/backfill.
  • Обмеження конкуренції (per-key serial) і fairness per tenant.
  • Метрики лага/помилок/latency, алерти на p95/p99 і зростання DLQ.
  • Версіонування схем і стратегія міграцій (v2 + світч).
  • Політики доступу/PII успадковані і перевірені.

Висновок

Read Models і проекції - це інженерний прискорювач читань: ви платите невеликою ціною «свіжості» та інфраструктури стрімінгу, щоб отримати передбачувані мілісекунди і розвантажити ядро записів. Проектуйте вітрини під запит, робіть апдейти ідемпотентними, вимірюйте лаг і явно обіцяйте свіжість - і ваші API залишаться швидкими навіть при зростанні навантаження, даних і географії.

Contact

Зв’яжіться з нами

Звертайтеся з будь-яких питань або за підтримкою.Ми завжди готові допомогти!

Розпочати інтеграцію

Email — обов’язковий. Telegram або WhatsApp — за бажанням.

Ваше ім’я необов’язково
Email необов’язково
Тема необов’язково
Повідомлення необов’язково
Telegram необов’язково
@
Якщо ви вкажете Telegram — ми відповімо й там, додатково до Email.
WhatsApp необов’язково
Формат: +код країни та номер (наприклад, +380XXXXXXXXX).

Натискаючи кнопку, ви погоджуєтесь на обробку даних.