В PostgreSQL при двух параллельных транзакциях, которые читают и обновляют одно и то же значение счетчика просмотров, возможна ситуация гонки (race condition).
// У нас есть таблица с товаром, у которого есть счетчик просмотра
В PostgreSQL при двух параллельных транзакциях, которые читают и обновляют одно и то же значение счетчика просмотров, возможна ситуация гонки (race condition).
Короткий ответ
Что ответить на собеседовании
Подробный разбор
Ответ с пояснениями
Условие
// У нас есть таблица с товаром, у которого есть счетчик просмотра // table goods: // ID | cnt_view // ----------- // 1 | 100
// 2 пользователя одновременно начали смотреть товар с id = 1 // и у нас в базе запустилось 2 параллельные транзакции // нужно объяснить какой будет результат? postgress
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()
}
Ответ
В PostgreSQL при двух параллельных транзакциях, которые читают и обновляют одно и то же значение счетчика просмотров, возможна ситуация гонки (race condition).
Что происходит:
- Обе транзакции начинают и читают текущее значение cnt_view = 100.
- Обе увеличивают локальную переменную cnt до 101.
- Обе пытаются записать cnt_view = 101 обратно в таблицу.
В итоге, несмотря на два просмотра, счетчик увеличится только на 1, а не на 2.
Это происходит потому, что транзакции не синхронизированы и не используют блокировки или атомарные операции.
Чтобы избежать этой проблемы, можно:
- Использовать оператор UPDATE с инкрементом напрямую, без предварительного чтения:
UPDATE goods SET cnt_view = cnt_view + 1 WHERE id = $1;
- Либо использовать блокировку строки (SELECT ... FOR UPDATE) перед чтением, чтобы гарантировать последовательный доступ.
- Либо использовать сериализуемый уровень изоляции транзакций, но это может снизить производительность.
Пример исправленной функции:
func IncrementView(ctx context.Context, db *sql.DB, id int64) error {
_, err := db.ExecContext(ctx, "UPDATE goods SET cnt_view = cnt_view + 1 WHERE id = $1", id)
return err
}
Таким образом, счетчик будет корректно увеличиваться при параллельных запросах.