← Назад к вопросам
Что будет, если попытаться прочитать из Slave информацию, которая успела записаться только в Master?
2.0 Middle🔥 171 комментариев
#Базы данных#Микросервисы и архитектура
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Чтение из Slave данных, которые есть только в Master
Основная проблема: Replication Lag
В типичной архитектуре с репликацией БД операции происходят асинхронно:
Vremennaya liniya:
[t=0] Master: INSERT user -> успешно записано
[t=0-5ms] Передача бинлога в Slave по сети
[t=5ms] Slave: применение транзакции
↑ В этом окне Slave ещё не видит данные
Если попытаться прочитать данные из Slave ДО того, как репликация завершилась, вы получите одно из двух:
Возможные результаты
1. Empty Result Set
// Пример: запись в Master
masterDB.Exec("INSERT INTO users (id, name) VALUES ($1, $2)", 1, "John")
// Немедленное чтение из Slave
var user User
err := slaveDB.QueryRow("SELECT * FROM users WHERE id = $1", 1).Scan(&user.ID, &user.Name)
// err == sql.ErrNoRows (данные ещё не здесь)
2. Stale Data (старые данные)
Если был UPDATE:
// Master: UPDATE users SET name = "Jane" WHERE id = 1
// Slave (лаг 100ms): SELECT * FROM users WHERE id = 1
// Результат: name = "John" (старое значение)
3. Partial Updates
При сложных транзакциях:
-- Master: двухэтапная транзакция
BEGIN;
INSERT INTO posts (user_id, title) VALUES (1, "Hello");
UPDATE users SET post_count = post_count + 1 WHERE id = 1;
COMMIT;
-- Slave (в середине репликации):
-- Видит новый пост, но counter ещё не обновлён
Как это влияет на приложение?
// Типичный баг при неправильной работе с replica
func CreatePostAndGetUserStats(userID int, postTitle string) (*UserStats, error) {
// 1. Пишем новый пост в Master
_, err := masterDB.Exec(
"INSERT INTO posts (user_id, title) VALUES ($1, $2)",
userID, postTitle,
)
if err != nil {
return nil, err
}
// 2. ❌ ОШИБКА: сразу читаем из Slave
stats, err := slaveDB.QueryRow(
"SELECT post_count FROM users WHERE id = $1",
userID,
).Scan(&stats.PostCount)
// Получим старый post_count! Новый пост ещё не реплицировался
return stats, nil
}
Решение 1: Читать из Master после Write
func CreatePostAndGetUserStats(userID int, postTitle string) (*UserStats, error) {
// 1. Пишем в Master
_, err := masterDB.Exec(
"INSERT INTO posts (user_id, title) VALUES ($1, $2)",
userID, postTitle,
)
if err != nil {
return nil, err
}
// 2. ✅ Читаем из того же Master
stats, err := masterDB.QueryRow(
"SELECT post_count FROM users WHERE id = $1",
userID,
).Scan(&stats.PostCount)
// Гарантированно свежие данные
return stats, nil
}
Решение 2: Sticky Connection (Pin to Master)
type DBRouter struct {
masterDB *sql.DB
slaveDB *sql.DB
sticky map[string]time.Time // userID -> когда вернуться на Slave
}
func (r *DBRouter) Write(query string, args ...interface{}) error {
// Всегда пишем в Master
return r.masterDB.Exec(query, args...).Err()
}
func (r *DBRouter) Read(userID string, query string) *sql.DB {
// Если недавно писали, читаем из Master
if lastWrite, exists := r.sticky[userID]; exists {
if time.Since(lastWrite) < 1*time.Second { // 1 второй лаг
return r.masterDB
}
delete(r.sticky, userID) // Истёк timeout
}
return r.slaveDB // Иначе читаем из Slave
}
func (r *DBRouter) WriteWithSticky(userID string, query string, args ...interface{}) error {
err := r.Write(query, args...)
if err == nil {
r.sticky[userID] = time.Now() // Отметить время записи
}
return err
}
Решение 3: Write-Through Cache
func CreatePost(userID int, title string) (*Post, error) {
// 1. Пишем в Master
post := &Post{ID: uuid.New(), UserID: userID, Title: title}
_, err := masterDB.Exec(
"INSERT INTO posts (id, user_id, title) VALUES ($1, $2, $3)",
post.ID, userID, title,
)
if err != nil {
return nil, err
}
// 2. Кешируем в Redis (не ждём репликации)
cacheKey := fmt.Sprintf("post:%s", post.ID)
cache.Set(cacheKey, post, 5*time.Minute)
// 3. Приложение читает из кеша
return cache.Get(cacheKey), nil
}
Решение 4: Используйте Synchronous Replication
Мастер: [WRITE] → [ждёт подтверждения] → [COMMIT] ← СИНХРОННО
(blocking ждёт Slave)
Слейв: [получает] → [применяет] → [отправляет ACK]
Инструменты:
- PostgreSQL с Patroni
- MySQL с Galera Cluster
- MariaDB с Galera
Best Practice Выход
const (
READ_MASTER = iota // Критичные данные сразу после write
READ_SLAVE // Некритичные данные (отчёты, аналитика)
READ_CACHE // Очень частые reads
)
func Query(ctx context.Context, preference int, query string) (*sql.Rows, error) {
var db *sql.DB
switch preference {
case READ_MASTER:
db = masterDB
case READ_CACHE:
// Попробовать кеш сначала
if cached := cache.Get(query); cached != nil {
return cached, nil
}
db = slaveDB
default:
db = slaveDB
}
return db.QueryContext(ctx, query)
}
Заключение
- Асинхронная репликация всегда имеет лаг (от миллисекунд до секунд)
- Чтение из Slave после Write может вернуть старые данные или отсутствие данных
- Решение: читайте из Master критичные данные, используйте кеш или sticky connections
- Мониторьте реальный replication lag в production — это критичная метрика