← Назад к вопросам
Как спроектировать сервис такси, чтобы такси не вызвалось дважды?
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 секунд
- Добавить подтверждение перед повторным запросом
Комбинированный подход (Рекомендуемый)
Фронтенд:
- Генерировать idempotency_key
- Отключить кнопку после клика
- Показать loading indicator
- Задать timeout и повторить с тем же ключом при ошибке
Бэкенд:
- Сохранять idempotency_key в Redis
- Проверить stateful: нет ли активного заказа у пользователя
- Использовать транзакцию для создания заказа
- Вернуть идентификатор заказа
База данных:
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 является наиболее надежным.