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

Как спроектировать сервис такси, чтобы такси не вызвалось дважды?

2.8 Senior🔥 142 комментариев
#Архитектура систем#Требования и их анализ

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

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

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

Проектирование сервиса такси с предотвращением дублирования вызовов

Это классическая задача на идемпотентность (idempotency) — гарантирование того, что повторное выполнение одной и той же операции даёт тот же результат. Рассмотрю несколько подходов к решению этой проблемы.

Основная проблема

Сценарий дублирования:

  • Пользователь нажимает кнопку "Вызвать такси"
  • Сеть медленная, ответ не приходит
  • Пользователь нажимает кнопку еще раз
  • Сервер создает два отдельных заказа вместо одного
  • Результат: два такси приезжают к клиенту

Подход 1: Идемпотентные ключи (Idempotency Keys)

Реализация:

  • Клиент генерирует уникальный UUID для каждого запроса (idempotency_key)
  • Отправляет его в заголовке: X-Idempotency-Key: {uuid}
  • Сервер сохраняет ключ → результат в cache/БД
  • Если тот же ключ придет еще раз, вернуть сохраненный результат

Пример:

POST /api/v1/rides
X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Body: {start: "123 Main St", end: "456 Oak Ave"}

Ответ первый раз: 201 Created {ride_id: "abc123"}
Ответ второй раз с тем же ключом: 200 OK {ride_id: "abc123"}

Хранение результатов:

  • Redis Cache (TTL 24 часа) для быстрого доступа
  • PostgreSQL таблица IdempotencyKey для долгосрочного хранения
  • Структура: {idempotency_key, user_id, method, endpoint, response_status, response_body, created_at}

Плюсы:

  • Стандартный подход, используемый Stripe, PayPal
  • Работает для любых операций
  • Безопасный и надежный

Минусы:

  • Требует координации между клиентом и сервером
  • Нужно хранить истории результатов
  • Усложняет логику обработки

Подход 2: Оптимистичные блокировки (Optimistic Locking)

Реализация:

  • Добавить версию (version/revision) к заказу
  • Проверить статус перед созданием нового заказа
  • Использовать транзакции с SERIALIZABLE уровнем

SQL пример:

BEGIN TRANSACTION;
SELECT * FROM rides WHERE user_id = ? AND status = 'pending';
-- Если уже есть pending заказ, откатить
IF row_exists THEN
  ROLLBACK;
ELSE
  INSERT INTO rides (user_id, start, end, status) VALUES (...);
  COMMIT;
END IF;

Подход 3: Пессимистичные блокировки (Pessimistic Locking)

Реализация:

  • SELECT FOR UPDATE для блокирования записи
  • Проверить статус с блокировкой
  • Если свободно — создать заказ
  • Если нет — вернуть ошибку
BEGIN TRANSACTION;
SELECT * FROM users WHERE id = ? FOR UPDATE;
-- Проверить активные заказы
SELECT * FROM rides WHERE user_id = ? AND status IN ('pending', 'accepted');
IF count > 0 THEN
  ROLLBACK;
ELSE
  INSERT INTO rides (...);
  COMMIT;
END IF;

Подход 4: Состояния и переходы (State Machine)

Реализация:

  • Таблица для отслеживания статусов заказов
  • Только определенные переходы разрешены
  • Попытка создать заказ когда уже есть активный → ERROR

Статусы:

  • searching — ищем такси
  • driver_found — водитель найден
  • in_progress — едем
  • completed — завершен
  • cancelled — отменен

Проверка:

INSERT INTO rides (...)
WHERE NOT EXISTS (
  SELECT 1 FROM rides 
  WHERE user_id = ?
  AND status NOT IN ('completed', 'cancelled')
);

Подход 5: Фронтенд решение

Технически менее надежно, но помогает:

  • Отключить кнопку "Вызвать такси" после первого клика
  • Показать loading state
  • Запретить повторный клик в течение N секунд
  • Добавить подтверждение перед повторным запросом

Комбинированный подход (Рекомендуемый)

Фронтенд:

  1. Генерировать idempotency_key
  2. Отключить кнопку после клика
  3. Показать loading indicator
  4. Задать timeout и повторить с тем же ключом при ошибке

Бэкенд:

  1. Сохранять idempotency_key в Redis
  2. Проверить stateful: нет ли активного заказа у пользователя
  3. Использовать транзакцию для создания заказа
  4. Вернуть идентификатор заказа

База данных:

CREATE TABLE idempotency_keys (
  id UUID PRIMARY KEY,
  user_id UUID NOT NULL,
  request_hash VARCHAR(255) NOT NULL,
  response_code INT,
  response_body JSONB,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  UNIQUE(user_id, request_hash)
);

CREATE TABLE rides (
  id UUID PRIMARY KEY,
  user_id UUID NOT NULL REFERENCES users(id),
  status VARCHAR(50) DEFAULT 'searching',
  start_location VARCHAR(255),
  end_location VARCHAR(255),
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  UNIQUE(user_id, created_at) -- Ограничение: один заказ в секунду
);

Дополнительные механизмы

Телеметрия и мониторинг:

  • Отслеживать повторные запросы
  • Логировать idempotency_key использование
  • Алерты при частых повторах (признак проблемы клиента)

Тестирование:

  • Тест на отправку одинаковых запросов
  • Проверка что ID заказа совпадает
  • Проверка что счет не выставлен дважды

Race conditions:

  • Использовать SELECT FOR UPDATE
  • Уровень изоляции SERIALIZABLE для критичных операций
  • Message Queue для асинхронной обработки

Правильное решение зависит от требований системы, но комбинированный подход с idempotency keys + state machine является наиболее надежным.

Как спроектировать сервис такси, чтобы такси не вызвалось дважды? | PrepBro