Проблема. Транзакция меняет несколько страниц в разных местах файла. Если записывать их сразу и питание пропадёт посередине, на диске окажется половина изменений — нарушится атомарность. А если ждать записи всех страниц при каждом коммите — это много случайных fsync.
Решение — журнал предзаписи:
- изменение сначала описывается записью в журнале (что, где, новое значение) — последовательное дописывание в конец файла;
- при коммите журнал сбрасывается на диск (fsync) до записи подтверждения клиенту — это и есть durability из ACID;
- изменённые страницы остаются в памяти (буферный кэш) и пишутся на диск позже, в фоне;
- периодически выполняется контрольная точка: все грязные страницы сбрасываются, и журнал до этого места больше не нужен для восстановления.
Восстановление после сбоя: с последней контрольной точки журнал читается и изменения повторяются (redo). Транзакции без записи о коммите отбрасываются (в PostgreSQL их версии строк просто невидимы благодаря MVCC; в других СУБД есть ещё undo-журнал для отката).
Почему это быстрее: запись в конец файла последовательная, а несколько коммитов можно сбросить одним fsync (group commit). Сами страницы данных пишутся реже и пачками.
Журнал как поток изменений:
- репликация — реплика получает журнал и применяет его (физическая потоковая репликация PostgreSQL, binlog в MySQL);
- восстановление на момент времени — базовая копия плюс архив журнала до нужной секунды;
- CDC — логическое декодирование журнала публикует изменения в Kafka (Debezium) без двойной записи из приложения.
Настройки компромисса: synchronous_commit = off в PostgreSQL не ждёт fsync при коммите — быстрее, но при падении можно потерять последние доли секунды транзакций (без порчи данных). innodb_flush_log_at_trx_commit в MySQL — то же самое.
Идея журнала повсюду: файловые системы с журналированием (ext4), Kafka (вся система — журнал), LSM-хранилища, Raft (реплицированный журнал).