Как избежать race condition при одновременном чтении и записи в таблицу транзакций в Go
Что проверяют
Различаете ли вы гонку в памяти процесса и гонку в базе данных: «таблица транзакций» может означать и слайс в структуре, и настоящую таблицу в PostgreSQL. Следом обычно спрашивают, как вы докажете, что гонки больше нет, и что будет, если сервис запущен в нескольких экземплярах.
Как отвечать
Сначала уточните, где лежат данные, и разберите оба случая. Если это структура в памяти:
sync.Mutexилиsync.RWMutex: все чтения и записи общего слайса идут под одним мьютексом.RWMutexимеет смысл, когда чтений заметно больше, чем записей;- копия на выходе: геттер возвращает
slices.Clone(s.items), а не сам слайс. Иначе вызывающий код читает тот же массив уже послеUnlock, и гонка возвращается; - горутина-владелец: состояние держит одна горутина, остальные общаются с ней через каналы, и мьютекс не нужен;
sync/atomic: для счётчиков и флагов, напримерatomic.Int64.
Если это таблица в базе, мьютекс в Go не поможет: у сервиса несколько реплик. Здесь работают средства СУБД:
- атомарный
UPDATE:UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1вместо «прочитать, посчитать в Go, записать»; SELECT ... FOR UPDATEвнутри транзакции изdb.BeginTx: строка заблокирована до коммита;- оптимистическая блокировка: колонка
version, запись с условиемWHERE version = $nи повтор, если обновлено ноль строк; - изоляция
Serializableс повтором транзакции при ошибке сериализации.
Проверку назовите сразу: go test -race находит гонки в памяти, для базы нужен тест с параллельными транзакциями.
Где спотыкаются
Возвращают внутренний слайс из-под мьютекса и считают, что защитили данные. Блокировка закончилась на return, а массив остался общим.
Предлагают sync.Mutex для таблицы в базе. Он работает внутри одного процесса, вторая реплика сервиса о нём ничего не знает.
Считают чистый прогон с -race гарантией. Детектор ловит только гонки, которые реально случились во время теста, поэтому тест должен нагружать код параллельно.