// tableau des produits: // ID | cnt_view // ----------- // 1 | 100 // deux utilisateurs ont commencé à regarder le produit avec id = 1 en même temps // et dans notre base de données, 2 transactions parallèles ont été lancées // quel sera le résultat? 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 IA
Réponse de l'IA
В приведённом коде две параллельные транзакции одновременно читают значение cnt_view для товара с id=1, увеличивают его на 1 и записывают обратно.
В PostgreSQL по умолчанию уровень изоляции транзакций — Read Committed. Это значит, что каждая транзакция видит только зафиксированные данные на момент начала запроса.
Сценарий:
- Обе транзакции читают cnt_view = 100.
- Обе увеличивают cnt_view до 101.
- Обе пытаются записать 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)
Это гарантирует последовательное обновление счётчика просмотров без потери данных.