> Что такое паттерн разделения на чтение и запись (Go)

Уровень: junior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Employcity

Стек: Go

> Пример ответа

Короткий ответ

Паттерн разделения на чтение и запись (read-write split) - это архитектурный подход, при котором операции чтения и записи направляются на разные реплики базы данных. Основная запись идёт на primary (master), а чтение распределяется между read replicas. Это позволяет горизонтально масштабировать нагрузку на чтение, снижая давление на основную базу. В Go это часто реализуется через пул соединений с разными DSN и middleware для маршрутизации запросов.

Подробное объяснение

Суть паттерна в том, что база данных с одной primary-нодой имеет ограниченную пропускную способность. Если приложение преимущественно читает данные (типичный сценарий - 80/20), то primary быстро упирается в瓶颈. Реплики решают эту проблему: они асинхронно синхронизируются с primary и обслуживают read-запросы.

Ключевые моменты:

  • Primary принимает все write-операции (INSERT, UPDATE, DELETE) и транзакции.
  • Replicas обслуживают SELECT-запросы, не требующие строгой актуальности данных.
  • Репликация обычно асинхронная, поэтому возможна небольшая задержка данных (replication lag).

В Go реализация обычно выглядит так:

  • Два пула соединений: один для primary, другой для реплик.
  • Middleware или обёртка над database/sql, которая анализирует тип запроса (SELECT или нет) и выбирает нужный пул.
  • Для транзакций всегда используется primary, так как внутри транзакции могут быть и чтения, и записи.

Важно понимать trade-off: вы получаете масштабируемость чтения, но платите согласованностью. Если бизнес-логика требует чтения сразу после записи (read-your-writes), нужно либо использовать primary для таких запросов, либо применять дополнительные механизмы (например, запоминать timestamp последней записи и направлять запросы на primary, пока реплика не догонит).

На практике

Для junior-позиции достаточно понимать базовую схему и уметь её описать. На практике в Go это выглядит так:

  1. Создаётся интерфейс DB с методами Exec, Query, QueryRow.
  2. Реализация readWriteSplitDB содержит два *sql.DB - primary и replica.
  3. Метод Query выбирает replica, Exec - primary.
  4. Для транзакций используется только primary.

Также важно упомянуть, что в реальных проектах часто используют готовые библиотеки (например, go-sql-driver/mysql с поддержкой multi-source) или прокси-слой (ProxySQL, pgpool), который сам решает, куда направить запрос. Но для собеседования достаточно показать понимание принципа.

Пример кода

GO
type DB struct {
primary *sql.DB
replica *sql.DB
}
func (d *DB) Exec(query string, args ...any) (sql.Result, error) {
return d.primary.Exec(query, args...)
}
func (d *DB) Query(query string, args ...any) (*sql.Rows, error) {
return d.replica.Query(query, args...)
}
func (d *DB) QueryRow(query string, args ...any) *sql.Row {
return d.replica.QueryRow(query, args...)
}
func (d *DB) BeginTx(ctx context.Context) (*sql.Tx, error) {
return d.primary.BeginTx(ctx, nil)
}

Как отвечать на собеседовании

Начните с определения: "это разделение запросов на запись (идут на primary) и чтение (идут на реплики)". Затем объясните зачем: "чтобы масштабировать нагрузку на чтение и не упираться в ресурсы одной базы". Упомяните про replication lag и что транзакции всегда идут на primary. Если спросят про Go - скажите про два пула соединений и выбор пула в зависимости от типа запроса. Не углубляйтесь в детали реализации репликации, если не спрашивают.

Что проверяет интервьюер

Интервьюер проверяет:

  • Понимаете ли вы базовые принципы работы баз данных (репликация, согласованность).
  • Умеете ли вы видеть trade-off между производительностью и актуальностью данных.
  • Знаете ли вы, как это реализуется в коде на Go (пулы соединений, интерфейсы).
  • Понимаете ли вы, когда этот паттерн применим, а когда нет (например, при высокой доле записей он не поможет).

Типичные ошибки

  • Говорить, что реплики синхронные - обычно это не так, и это важно для понимания задержек.
  • Предлагать отправлять все запросы на реплики, включая транзакции - это ломает консистентность.
  • Не упоминать проблему read-your-writes - если приложение пишет и сразу читает, данные могут быть не видны на реплике.
  • Путать паттерн с шардированием - это разные вещи: шардирование делит данные по ключу, а read-write split дублирует данные на репликах.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью