← Назад к вопросам

Что будет, если попытаться прочитать из 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 — это критичная метрика
Что будет, если попытаться прочитать из Slave информацию, которая успела записаться только в Master? | PrepBro