Как работают параллельные транзакции с одной строкой при уровне изоляции SERIALIZABLE
Что проверяют
Понимаете ли вы, что произойдёт с двумя горутинами, которые в SERIALIZABLE-транзакциях трогают одну строку, и готов ли ваш код к тому, что одна из них получит ошибку. Уточнение почти всегда одно: покажите, как вы делаете повтор.
Как отвечать
SERIALIZABLE гарантирует эквивалентность какому-то последовательному порядку, а механизм зависит от базы. Сценарии для PostgreSQL, самой частой базы в Go-сервисах:
- обе делают
UPDATEодной строки: вторая встаёт в ожидание блокировки. Откат первой отпускает её дальше, коммит первой даёт второй ошибку с кодом40001; - одна читает, вторая меняет: ожидания нет, чтение идёт по снимку, а факт чтения база запоминает неблокирующей предикатной блокировкой;
- обе прочитали и записали разное на основе прочитанного: если такой порядок невозможен последовательно, одна транзакция падает с
40001, иногда только наCommit().
В MySQL иначе: InnoDB на этом уровне превращает чтения в разделяемые блокировки, и типичный итог двух конкурентных «прочитал и обновил» это дедлок с ошибкой 1213.
В Go уровень задают при открытии транзакции: db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable}) или в pgx pgx.TxOptions{IsoLevel: pgx.Serializable}. Ошибку разбирают по коду, а не по тексту: errors.As(err, &pgErr) с *pgconn.PgError и сравнение pgErr.Code с "40001". Каркас повтора: тело транзакции в функции func(tx pgx.Tx) error, цикл на несколько попыток, экспоненциальная пауза с джиттером, проверка ctx.Err() между попытками. На каждой попытке транзакция открывается заново и все чтения выполняются снова.
Где спотыкаются
Утверждают, что читающая транзакция заблокирует пишущую. В PostgreSQL этого нет, вместо ожидания вы получите откат.
Проверяют ошибку через strings.Contains(err.Error(), ...) или сравнением err ==. Драйвер оборачивает ошибки, надёжно работает только errors.As.
Держат транзакцию открытой, пока горутина ждёт сеть или соседний сервис. Окно конфликта растёт, доля откатов тоже, а соединение из пула всё это время занято.