Опирается на правила: PG-RP-001PG-RP-085 из PostgreSQL Style Guide → раздел Репликация.

Важно знать

  • Streaming replication: master пишет WAL → реплика проигрывает.
  • Replication lag 50-500ms норма, до секунд под нагрузкой.
  • Сценарии: разгрузка мастера, HA, геораспределение.
  • Не для read-after-write — реплика отстаёт.
  • AbstractRoutingDataSource + LazyConnectionDataSourceProxy — routing по readOnly.
  • Без LazyConnection Spring выбирает DataSource до знания readOnly.
  • Read-after-write решается: read-from-master, возврат из write, wait for catch-up.
  • Synchronous replication — обычно не нужно (latency commit).
  • Failover — HikariCP переподключается; retry на критичных.
  • Алёрт на replay_lag > 30 сек или > 1 GB WAL.

PostgreSQL streaming replication — master пишет WAL, реплика проигрывает. Для разработчика главный вопрос — как и когда читать с реплики.

Архитектура

PG-RP-001..002:

  • Master (primary) — single source of truth, все INSERT/UPDATE/DELETE.
  • Replica (standby, hot standby) — проигрывает WAL, read-only.

PG поддерживает async (default) и sync. Для backend чаще async — replica может отстать на ms-секунды.

Replication lag: 50-500ms норма; под нагрузкой / большими транзакциями — секунды.

Сценарии

PG-RP-010..011:

Использовать:

  • Разгрузка мастера — тяжёлые SELECT (отчёты, аналитика) не конкурируют с OLTP.
  • HA — failover при падении мастера.
  • Геораспределение — реплика рядом с пользователем.

Не использовать:

  • Read-after-write в одном пользовательском действии.
  • Операции с мгновенной консистентностью.

Routing

PG-RP-020..022: Spring AbstractRoutingDataSource.

public enum DataSourceType { MASTER, REPLICA }

@Component
public class TransactionRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
            ? DataSourceType.REPLICA
            : DataSourceType.MASTER;
    }
}

@Configuration
public class DataSourceConfig {

    @Bean @Primary
    public DataSource routingDataSource(DataSource master, DataSource replica) {
        var routing = new TransactionRoutingDataSource();
        routing.setTargetDataSources(Map.of(
            DataSourceType.MASTER, master,
            DataSourceType.REPLICA, replica
        ));
        routing.setDefaultTargetDataSource(master);
        return new LazyConnectionDataSourceProxy(routing);   // обязательно!
    }
}

PG-RP-021: LazyConnectionDataSourceProxy обязателен. Без него Spring выбирает DataSource в момент открытия TX, до того как становится известно readOnly. С Lazy — откладывается до первого реального запроса.

@Transactional(readOnly = true)
public List<OrderView> findOrders(...) {
    // на replica
}

@Transactional
public OrderId createOrder(...) {
    // на master
}

Read-after-write — антипаттерн

PG-RP-030..031:

// ✗ — на реплике без свежего заказа
public OrderResponse handle(CreateOrderRequest req) {
    var id = service.createOrder(req);       // на master
    var orders = service.myOrders(req.customerId());   // на replica — без только что созданного
    return ...;
}

Решения:

A. Read-from-master

@Transactional   // без readOnly — пойдёт на master
public List<Order> myOrdersFromMaster(long customerId) { ... }

B. Возвращать данные сразу из write

public OrderResponse createOrder(...) {
    var saved = orderRepo.save(...);
    return OrderResponse.from(saved);   // данные уже в руке
}

C. Wait for replica catch-up

String lsn = jdbc.queryForObject("SELECT pg_current_wal_lsn()", String.class);

do {
    String replayLsn = replicaJdbc.queryForObject("SELECT pg_last_wal_replay_lsn()", String.class);
    if (lsnGte(replayLsn, lsn)) break;
    Thread.sleep(50);
} while (true);

Сложно, специфические случаи. Обычно A или B.

Synchronous replication

PG-RP-040..042:

# postgresql.conf на мастере
synchronous_commit = on
synchronous_standby_names = 'replica1, replica2'

Мастер ждёт подтверждения реплики перед возвратом OK на COMMIT.

Цена — latency COMMIT увеличивается на network round-trip + replica fsync (1-5ms локально, десятки ms geo).

Обычно не нужно. Async + правильный routing решает 99%. Sync — только для критических данных «после COMMIT обязательно на двух дисках».

Failover

PG-RP-050..052:

1. Failover-инструмент (Patroni, repmgr) обнаруживает падение мастера.
2. Promote одной из реплик в master.
3. DNS / LB / VIP переключаются.
4. Приложение переподключается.

Spring + HikariCP справится при правильных validationTimeout и connectionTestQuery. После failover соединения инвалидируются, открываются к новому мастеру.

На время failover (10-60 сек) — ошибки writes. Retry на критичных:

@Retryable(retryFor = SQLException.class, maxAttempts = 5, backoff = @Backoff(delay = 1000, multiplier = 2))

Logical replication

PG-RP-060..062: копирует изменения по таблицам, не WAL.

Можно реплицировать подмножество таблиц, в другую схему, с трансформацией.

Применения:

  • Гибридный стек (PG → DWH/Kafka).
  • Online migration с PG на PG.
  • Multi-master (с conflict resolution).

Для read-replica с разгрузкой — обычная streaming replication. Logical имеет больший overhead.

Мониторинг lag

PG-RP-070..072:

На мастере:

SELECT application_name, state, replay_lag
FROM pg_stat_replication;

На реплике:

SELECT now() - pg_last_xact_replay_timestamp() AS replication_lag;

Алёрт на replay_lag > 30 сек или > 1 GB WAL pending.

Что запрещено

АнтипаттернПравилоЧто взамен
Read-after-write через репликуPG-RP-080read-from-master
Все SELECT на реплику без readOnlyPG-RP-081явный @Transactional(readOnly = true)
AbstractRoutingDataSource без LazyConnectionPG-RP-082обязателен Lazy
Sync replication «на всякий случай»PG-RP-083async обычно
Игнорировать replay_lag в мониторингеPG-RP-084алёрт > 30 сек
Sticky-session по userIdPG-RP-085не покрывает cross-user
logical для read-replica разгрузкиPG-RP-062streaming

Куда дальше

  • PG → Репликация — нормативные формулировки.
  • Spring @Transactional — readOnly детали.
  • HikariCP — отдельные пулы.
  • Уровни изоляции — read-only сессии.
  • WAL — что лагает.
  • Distributed → eventual consistency — read-your-writes.