← Назад к вопросам

Сталкивался ли с race condition при работе с запросами к API

2.0 Middle🔥 152 комментариев
#JavaScript Core

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Race Condition при работе с запросами к API: практический опыт и решения

Да, сталкивался многократно. Race condition (состояние гонки) — это классическая проблема параллельного выполнения, возникающая когда результат операции зависит от непредсказуемого порядка завершения нескольких одновременных или перекрывающихся процессов. В контексте API запросов это проявляется, когда несколько независимых запросов, выполняемых параллельно (например, из разных компонентов или в результате пользовательских действий), конкурируют за общие ресурсы или изменяют состояние приложения/сервера в неожиданном порядке.

Типичные сценарии в фронтенде

  1. Конкуренция при изменении состояния 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 может вернуться позже.
      // Финальное состояние профиля становится неопределенным.
    }
    
  2. Несколько запросов на чтение, влияющих на UI

    // Запросы на получение списка пользователей и их детальной информации
    // могут возвращать данные в разном порядке, вызывая "прыжки" в интерфейсе
    useEffect(() => {
      fetchUsers().then(users => setState(users)); // Запрос 1
      fetchUserDetails().then(details => setState(details)); // Запрос 2
    }, []);
    // Если details придут раньше users, компонент может попытаться отобразить детали,
    // ссылаясь на несуществующий в state список пользователей.
    
  3. Конкуренция при отправке форм или повторных кликах пользователя Частая ситуация: пользователь быстро кликает "Сохранить" дважды → два одинаковых 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.