Сталкивался ли с race condition при работе с запросами к API
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Race Condition при работе с запросами к API: практический опыт и решения
Да, сталкивался многократно. Race condition (состояние гонки) — это классическая проблема параллельного выполнения, возникающая когда результат операции зависит от непредсказуемого порядка завершения нескольких одновременных или перекрывающихся процессов. В контексте API запросов это проявляется, когда несколько независимых запросов, выполняемых параллельно (например, из разных компонентов или в результате пользовательских действий), конкурируют за общие ресурсы или изменяют состояние приложения/сервера в неожиданном порядке.
Типичные сценарии в фронтенде
-
Конкуренция при изменении состояния UI/данных
// Пример: два запроса на обновление профиля запускаются почти одновременно async function updateProfile(data1, data2) { // Запрос 1 (из одного места в коде) const response1 = await fetch('/api/profile', { method: 'PUT', body: JSON.stringify(data1) }); // Запрос 2 (из другого компонента, запущенный в тот же момент) const response2 = await fetch('/api/profile', { method: 'PUT', body: JSON.stringify(data2) }); // Кто завершится первым? Сервер может получить data2 раньше data1, // но из-за сетевой латентности ответ на data1 может вернуться позже. // Финальное состояние профиля становится неопределенным. } -
Несколько запросов на чтение, влияющих на UI
// Запросы на получение списка пользователей и их детальной информации // могут возвращать данные в разном порядке, вызывая "прыжки" в интерфейсе useEffect(() => { fetchUsers().then(users => setState(users)); // Запрос 1 fetchUserDetails().then(details => setState(details)); // Запрос 2 }, []); // Если details придут раньше users, компонент может попытаться отобразить детали, // ссылаясь на несуществующий в state список пользователей. -
Конкуренция при отправке форм или повторных кликах пользователя Частая ситуация: пользователь быстро кликает "Сохранить" дважды → два одинаковых POST/PUT запроса отправляются параллельно. Сервер может обработать второй запрос раньше первого (из-за особенностей маршрутизации, нагрузки), что приводит к дублированию данных или конфликту версий.
Методы предотвращения и решения
1. Дедублирование запросов (Debouncing/Throttling)
// Использование debounce для предотвращения множественных отправок
import { debounce } from 'lodash';
const debouncedSave = debounce(async (data) => {
await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) });
}, 500); // Запрос будет отправлен только если между событиями прошло 500ms
// Применение в компоненте
handleChange = (data) => debouncedSave(data);
2. Отмена предыдущих запросов (AbortController)
// Создание и использование AbortController для отмены предыдущего запроса
let abortController = null;
async function fetchSearchResults(query) {
if (abortController) {
abortController.abort(); // Отменяем предыдущий запрос
}
abortController = new AbortController();
try {
const response = await fetch(`/api/search?q=${query}`, {
signal: abortController.signal
});
return response.json();
} catch (err) {
if (err.name === 'AbortError') {
console.log('Запрос отменен'); // Игнорируем ошибку отмены
} else {
throw err;
}
}
}
3. Очереди запросов и мьютексы (Mutex)
// Реализация простой очереди с использованием async/await
let isFetching = false;
async function exclusiveUpdate(data) {
if (isFetching) {
// Ждем завершения текущего запроса
await new Promise(resolve => setTimeout(resolve, 100)); // Или более сложная логика ожидания
}
isFetching = true;
try {
const result = await fetch('/api/update', { method: 'PUT', body: JSON.stringify(data) });
isFetching = false;
return result;
} catch (err) {
isFetching = false;
throw err;
}
}
4. Оптимистичные обновления (Optimistic Updates) с версионированием
// При отправке данных сначала обновляем UI, затем отправляем запрос с версией
// Сервер проверяет версию и отвергает запрос, если данные уже были изменены другим запросом
async function optimisticUpdate(data, currentVersion) {
// 1. Сразу обновляем локальное состояние
setLocalState(data);
// 2. Отправляем запрос с контрольной версией
const response = await fetch('/api/update', {
method: 'PUT',
body: JSON.stringify({ data, version: currentVersion })
});
// 3. Если сервер вернул конфликт (409 Conflict), обновляем UI с актуальными данными
if (response.status === 409) {
const latestData = await fetch('/api/latest');
setLocalState(latestData);
}
}
5. Использование состояний запросов (Request States)
// Хранение состояния каждого запроса и проверка перед выполнением
const requestStates = {};
async function makeRequest(key, url) {
if (requestStates[key] === 'pending') {
console.log(`Запрос ${key} уже выполняется`);
return;
}
requestStates[key] = 'pending';
try {
const result = await fetch(url);
requestStates[key] = 'completed';
return result;
} catch (err) {
requestStates[key] = 'failed';
throw err;
}
}
Архитектурные подходы
- Centralized API Service: создание единого сервиса для управления запросами, который внутренне контролирует параллельность.
- React Query / SWR: использование библиотек, которые предоставляют内置 механизмы дедублирования, отмены и управления состоянием запросов.
- Server-side решения: применение версионирования ресурсов (ETag), блокировок (Locking) и транзакций на сервере для предотвращения конфликтов при параллельных операциях.
Race condition в API запросах — не просто теоретическая проблема. Она приводит к реальным багам: дублирование данных в БД, некорректное отображение интерфейса, потеря обновлений. Решение требует комбинации клиентских (дедублирование, отмена) и серверных (версионирование, транзакции) техник. Наиболее эффективный подход — предвидеть возможность конкуренции в критических операциях (обновления, сохранения) и заранее внедрять защитные механизмы, а не ждать появления багов в production.