Sobes.tech
Middle+

// таблица с товарами: // ID | cnt_view // ----------- // 1 | 100 // два пользователя начали одновременно просматривать товар с id = 1 // и в нашей базе данных запущены 2 параллельные транзакции // какой будет результат? postgresql func IncrementView(ctx context.Context, db *sql.DB, id int64) error { tx, err := db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() var cnt int err = tx.QueryRowContext(ctx, "SELECT cnt_view FROM goods WHERE id = $1", id, ).Scan(&cnt) if err != nil { return err } cnt++ _, err = tx.ExecContext(ctx, "UPDATE goods SET cnt_view = $1 WHERE id = $2", cnt, id, ) if err != nil { return err } return tx.Commit() }

sobes.tech АИ

Одговор од АИ

В приведённом коде две параллельные транзакции одновременно читают значение cnt_view для товара с id=1, увеличивают его на 1 и записывают обратно.

В PostgreSQL по умолчанию уровень изоляции транзакций — Read Committed. Это значит, что каждая транзакция видит только зафиксированные данные на момент начала запроса.

Сценарий:

  1. Обе транзакции читают cnt_view = 100.
  2. Обе увеличивают cnt_view до 101.
  3. Обе пытаются записать 101.

Проблема — потеря обновления (lost update). Вторая транзакция при попытке коммита получит ошибку блокировки (serialization failure) или будет ждать, пока первая завершится.

Чтобы избежать этого, нужно использовать блокировку строки при чтении, например, SELECT ... FOR UPDATE, чтобы вторая транзакция ждала завершения первой и затем прочитала уже обновлённое значение.

Пример исправления:

err = tx.QueryRowContext(ctx, "SELECT cnt_view FROM goods WHERE id = $1 FOR UPDATE", id).Scan(&cnt)

Это гарантирует последовательное обновление счётчика просмотров без потери данных.