> Как избежать race condition при одновременном чтении и записи в таблицу транзакций в Go (Go)
Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы
Компании: Employcity
Стек: Go
> Пример ответа
Короткий ответ
Race condition при работе с таблицей транзакций в Go решается комбинацией уровней: на уровне БД - транзакции с правильным уровнем изоляции (например, SERIALIZABLE или SELECT ... FOR UPDATE), на уровне приложения - мьютексы или атомарные операции, если речь о in-memory состоянии. Для распределённых систем - оптимистичная блокировка с версионированием строк. Ключевой принцип: не полагаться на "проверка-затем-действие" без атомарности.
Подробное объяснение
Race condition возникает, когда две горутины или два сервиса одновременно читают и пишут одно состояние без синхронизации. В контексте таблицы транзакций есть два уровня:
-
Уровень БД - если транзакции хранятся в PostgreSQL/MySQL, гонка возникает между SQL-запросами. Здесь нужны:
- транзакции с уровнем изоляции
READ COMMITTEDили выше; - явные блокировки строк через
SELECT ... FOR UPDATE; - оптимистичная блокировка: поле
version, при апдейте проверяемWHERE version = ?, если затронуто 0 строк - retry.
- транзакции с уровнем изоляции
-
Уровень приложения - если есть кэш или 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 int64var version interr = 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 + deltares, 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.Mutexbalances map[int64]int64}func (s *AccountStore) UpdateBalance(id int64, delta int64) error {s.mu.Lock()defer s.mu.Unlock()s.balances[id] += deltareturn nil}
Как отвечать на собеседовании
Начни с уточнения: "Речь о гонке на уровне БД или в памяти приложения?" Это покажет системное мышление. Затем опиши два уровня защиты, приведи пример с SELECT ... FOR UPDATE и оптимистичной блокировкой. Упомяни trade-off: пессимистичная блокировка проще, но снижает конкурентность; оптимистичная - быстрее при низкой конкуренции, но требует retry. Если спросят про распределённые системы - скажи про распределённые блокировки или изоляцию на уровне БД.
Что проверяет интервьюер
- понимание разницы между гонкой на уровне приложения и БД;
- знание уровней изоляции транзакций;
- умение выбирать между пессимистичной и оптимистичной блокировкой;
- знание
sync.Mutexиsync/atomicи их ограничений; - способность проектировать надёжный код с retry.
Типичные ошибки
- использование
sync/atomicдля составных операций (проверка + запись) - не атомарно; - игнорирование мультиинстансности: мьютекс работает только в рамках одного процесса;
- отсутствие retry при оптимистичной блокировке - пользователь получает ошибку вместо повторной попытки;
- забывают про
FOR UPDATEвнутри транзакции - без него блокировка не сработает; - смешивают уровни: пытаются решить проблему БД мьютексом в приложении, когда несколько сервисов пишут в одну таблицу.
> Похожие задачи по Go
Можно ли отключить или ограничить запуск сборщика мусора в Go
Что такое кардинальность колонок и зачем это знать
Для чего нужно логирование
Когда логирование хорошо использовать, а когда лучше не использовать
> Похожие задачи по backend
Можно ли отключить или ограничить запуск сборщика мусора в Go
Что такое кардинальность колонок и зачем это знать
Для чего нужно логирование
Когда логирование хорошо использовать, а когда лучше не использовать
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью