Опирается на правила:
PG-RP-001…PG-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-080 | read-from-master |
Все SELECT на реплику без readOnly | PG-RP-081 | явный @Transactional(readOnly = true) |
AbstractRoutingDataSource без LazyConnection | PG-RP-082 | обязателен Lazy |
| Sync replication «на всякий случай» | PG-RP-083 | async обычно |
Игнорировать replay_lag в мониторинге | PG-RP-084 | алёрт > 30 сек |
| Sticky-session по userId | PG-RP-085 | не покрывает cross-user |
logical для read-replica разгрузки | PG-RP-062 | streaming |
Куда дальше
- PG → Репликация — нормативные формулировки.
- Spring @Transactional —
readOnlyдетали. - HikariCP — отдельные пулы.
- Уровни изоляции — read-only сессии.
- WAL — что лагает.
- Distributed → eventual consistency — read-your-writes.