> Как избежать race condition при одновременном чтении и записи в таблицу транзакций в Go (Go)

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

Компании: Employcity

Стек: Go

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

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

Race condition при работе с таблицей транзакций в Go решается комбинацией уровней: на уровне БД - транзакции с правильным уровнем изоляции (например, SERIALIZABLE или SELECT ... FOR UPDATE), на уровне приложения - мьютексы или атомарные операции, если речь о in-memory состоянии. Для распределённых систем - оптимистичная блокировка с версионированием строк. Ключевой принцип: не полагаться на "проверка-затем-действие" без атомарности.

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

Race condition возникает, когда две горутины или два сервиса одновременно читают и пишут одно состояние без синхронизации. В контексте таблицы транзакций есть два уровня:

  1. Уровень БД - если транзакции хранятся в PostgreSQL/MySQL, гонка возникает между SQL-запросами. Здесь нужны:

    • транзакции с уровнем изоляции READ COMMITTED или выше;
    • явные блокировки строк через SELECT ... FOR UPDATE;
    • оптимистичная блокировка: поле version, при апдейте проверяем WHERE version = ?, если затронуто 0 строк - retry.
  2. Уровень приложения - если есть кэш или in-memory структура, используется sync.Mutex, sync.RWMutex или атомарные операции из sync/atomic. Для сложных композитных операций (например, "прочитать баланс, проверить лимит, записать") мьютекс обязателен.

Для распределённых систем (несколько инстансов) мьютексы не работают - нужна распределённая блокировка (Redis, etcd) или изоляция на уровне БД.

На практике

Типичный сценарий: сервис обрабатывает платёжные транзакции. Каждая транзакция меняет баланс счёта. Если два запроса одновременно читают баланс и пишут новое значение - потеря обновления.

Практические рекомендации:

  • для одиночного инстанса - sync.Mutex вокруг критической секции;
  • для мультиинстансности - SELECT ... FOR UPDATE внутри SQL-транзакции;
  • если допустимы редкие конфликты - оптимистичная блокировка с retry (обычно 3-5 попыток);
  • не использовать sync/atomic для составных операций (проверка + запись) - это не атомарно.

Пример кода

GO
// Оптимистичная блокировка на уровне БД
func UpdateBalance(db *sql.DB, accountID int64, delta int64) error {
for attempt := 0; attempt < 3; attempt++ {
tx, err := db.BeginTx(ctx, nil)
if err != nil { return err }
var balance int64
var version int
err = tx.QueryRowContext(ctx,
"SELECT balance, version FROM accounts WHERE id = $1 FOR UPDATE",
accountID).Scan(&balance, &version)
if err != nil { tx.Rollback(); return err }
newBalance := balance + delta
res, err := tx.ExecContext(ctx,
"UPDATE accounts SET balance = $1, version = $2 WHERE id = $3 AND version = $4",
newBalance, version+1, accountID, version)
if err != nil { tx.Rollback(); return err }
affected, _ := res.RowsAffected()
if affected == 0 {
tx.Rollback()
continue // конфликт - retry
}
return tx.Commit()
}
return errors.New("конфликт обновления, попытки исчерпаны")
}
GO
// Мьютекс для in-memory состояния
type AccountStore struct {
mu sync.Mutex
balances map[int64]int64
}
func (s *AccountStore) UpdateBalance(id int64, delta int64) error {
s.mu.Lock()
defer s.mu.Unlock()
s.balances[id] += delta
return nil
}

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

Начни с уточнения: "Речь о гонке на уровне БД или в памяти приложения?" Это покажет системное мышление. Затем опиши два уровня защиты, приведи пример с SELECT ... FOR UPDATE и оптимистичной блокировкой. Упомяни trade-off: пессимистичная блокировка проще, но снижает конкурентность; оптимистичная - быстрее при низкой конкуренции, но требует retry. Если спросят про распределённые системы - скажи про распределённые блокировки или изоляцию на уровне БД.

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

  • понимание разницы между гонкой на уровне приложения и БД;
  • знание уровней изоляции транзакций;
  • умение выбирать между пессимистичной и оптимистичной блокировкой;
  • знание sync.Mutex и sync/atomic и их ограничений;
  • способность проектировать надёжный код с retry.

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

  • использование sync/atomic для составных операций (проверка + запись) - не атомарно;
  • игнорирование мультиинстансности: мьютекс работает только в рамках одного процесса;
  • отсутствие retry при оптимистичной блокировке - пользователь получает ошибку вместо повторной попытки;
  • забывают про FOR UPDATE внутри транзакции - без него блокировка не сработает;
  • смешивают уровни: пытаются решить проблему БД мьютексом в приложении, когда несколько сервисов пишут в одну таблицу.

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

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