Настоящее приложение почти никогда не держит данные в одном месте. Рядом с основной базой обычно живут кеш, поисковый индекс, аналитическое хранилище, денормализованные таблицы, read-модели. В этом зоопарке легко утонуть и перестать понимать, кто кому хозяин и какой копии верить при расхождении.
Есть простая рамка, которая сразу наводит порядок: разделить все хранилища на два вида — систему записи и производные данные. Это статья «для кругозора»: она не про конкретную технологию, а про то, как перестать путаться в собственной архитектуре.
Источник правды и его отражения
Система записи (source of truth, источник правды) — это хранилище, где факт лежит ровно один раз и в авторитетном виде. Обычно это ваша нормализованная база: пользователь оформил заказ — заказ записан сюда. И если данные в источнике вдруг разошлись с данными в любом другом хранилище, правым по определению считается источник.
Производные данные — это результат преобразования данных из источника, который всегда можно пересчитать заново. Сюда попадает почти всё остальное:
- кеш — быстрая копия части данных;
- поисковый индекс (Elasticsearch, GIN) — те же данные, переупакованные под поиск по тексту;
- материализованное представление или денормализованное поле — заранее посчитанный результат;
- read-модель в CQRS — данные, разложенные ровно под конкретный экран;
- аналитическое хранилище (ClickHouse) — те же события, но под сканы.
Ключевой признак производных данных — избыточность: они дублируют то, что уже есть в источнике, ради скорости чтения. Одно и то же можно отразить разными способами, поэтому на один источник обычно приходится несколько производных наборов.
Главное свойство: производное можно выбросить
Из этого определения следует практический вывод, который меняет отношение к данным: производные данные одноразовы. Потеряли кеш, индекс или read-модель — не беда: пересобрали из источника, и система как новенькая. А вот потеряли саму систему записи — вот это настоящая беда, восстанавливать неоткуда.
Отсюда вытекает всё остальное. Бэкапить и защищать в первую очередь нужно источник правды. У каждого производного набора должна быть процедура пересборки с нуля (переиндексировать, прогреть кеш, перестроить проекцию). А фраза «после выката пропала схема данных» перестаёт быть катастрофой, если пропала всего лишь проекция — её просто пересобирают. Понимание, что у тебя источник, а что производное, — это уже половина ясной архитектуры.
Три способа получать производные данные
Как именно данные текут из источника в производные наборы? Есть три режима обработки, и различать их полезно:
- Online (сервис). Ждёт запрос и отвечает как можно быстрее; главное — время отклика и доступность. Так работает само приложение. Но для получения больших производных наборов online не подходит.
- Batch (пакетная обработка). Берёт большой набор данных на вход, молотит его минуты или часы, выдаёт результат; главное тут — пропускная способность, а запускается это по расписанию (ночная переиндексация, пересчёт витрины). Классика больших данных — MapReduce/Spark; в обычном бэкенде проще — фоновый обработчик на очереди.
- Stream (потоковая обработка). Нечто среднее: реагирует на события по мере их поступления, с низкой задержкой, но обрабатывает не весь набор сразу, а поток изменений. Так производные данные держат свежими почти в реальном времени.
Batch пересобирает всё целиком и надёжен, но с задержкой; stream держит производное свежим, но устроен сложнее. Выбор между ними — отдельная большая тема.
Дисциплина: деривировать, а не дублировать записью
Главная ошибка при работе с производными данными — писать в них напрямую, в обход источника. Классический пример — двойная запись: приложение пишет и в базу, и в кеш (или в поисковый индекс) двумя отдельными операциями. Рано или поздно одна операция пройдёт, а вторая упадёт — и данные молча разъедутся.
Правильный принцип: писать только в систему записи, а всё производное выводить из неё — единым потоком изменений. Тогда у всех производных наборов один общий порядок обновлений, они согласованы между собой и легко пересобираются. Технически это делают событиями и outbox-паттерном или через CDC (change data capture — чтение журнала изменений базы). На том же принципе стоит event sourcing: источник правды — это поток событий, а всё остальное (проекции, read-модели) выводится из него.
Где это применяется
Как только рядом с базой появляется кеш, поисковый индекс или отдельная витрина — вы уже управляете производными данными, даже если не называли их так. Практическая рамка: явно назовите, что у вас источник правды, а что производное; для каждого производного набора заведите процедуру пересборки; и никогда не пишите в производное в обход источника — только выводите из единого потока изменений.
Где спотыкаются начинающие:
- Двойная запись в базу и в кеш/индекс — две операции без общей транзакции рано или поздно разойдутся. Пишем в источник, производное выводим (события/outbox/CDC).
- Нет процедуры пересборки производного набора — и «переиндексировать с нуля» вдруг оказывается невозможным, а потеря индекса превращается в инцидент на ровном месте.
- Бэкапят всё одинаково — не отделяя незаменимый источник правды от одноразовых производных, и тратят силы не туда.
- Считают read-модель или денормализованное поле «второй правдой» — и при расхождении начинают гадать, кто прав. Прав всегда источник.
Что почитать дальше: read-модель в CQRS — производные данные под конкретный экран; инвалидация кеша — как держать производную копию свежей; event sourcing — поток событий как источник правды; двойная запись — почему нельзя писать в два места сразу.