Опасность двойного клика
В распределенных корпоративных системах надежность сети — это миф. Пользователь с нестабильным мобильным интернетом нажимает кнопку «Отправить платеж» на вашей SaaS-платформе. HTTP-запрос доходит до вашего сервера на Laravel, приложение списывает средства с его кредитной карты через Stripe, и база данных обновляется. Однако ровно в тот момент, когда сервер отправляет ответ 200 OK обратно клиенту, мобильная сеть пользователя обрывается.
Браузер пользователя так и не получает сообщение об успешном завершении. Полагая, что приложение зависло, пользователь лихорадочно нажимает кнопку «Отправить платеж» еще три раза. Поскольку ваш API просто обрабатывает всё, что получает, теперь вы списали с клиента деньги четыре раза за один инвентарь (счет). В финансовых, е-коммерс или юридических приложениях такие непреднамеренные изменения катастрофичны и ведут к чарджбэкам, разрушению целостности данных и потере доверия клиентов.
В Smart Tech Devs мы создаем бэкенд-архитектуру, невосприимчивую к случайным повторным запросам. Мы достигаем этого за счет внедрения идемпотентности (Idempotency) на уровне API. Идемпотентный API гарантирует, что независимо от того, сколько раз клиент — безопасно или нет — повторяет абсолютно тот же запрос, мутация на бэкенде произойдет ровно один раз.
Понимание ключа идемпотентности
Чтобы реализовать эту архитектуру, клиент (фронтенд-приложение на React или мобильное приложение) должен сгенерировать уникальную строку (UUID v4), известную как Idempotency-Key , и прикрепить ее к заголовкам всех запросов POST , PUT или PATCH .
Когда сервер на Laravel получает запрос, он сверяет этот ключ с высокоскоростным централизованным кэшем (например, Redis).
- Если ключ встречается впервые, Laravel обрабатывает запрос в обычном порядке, сохраняет финальный HTTP-ответ в Redis, привязав его к этому ключу, и возвращает ответ пользователю.
- Если ключ уже существует (что означает повторный запрос), Laravel полностью пропускает логику контроллера. Он мгновенно извлекает сохраненный HTTP-ответ из Redis и возвращает его пользователю.
Фаза 1: Проектирование Middleware
Мы не хотим засорять наши контроллеры логикой идемпотентности. Вместо этого мы проектируем глобальное или специфичное для роутов Middleware, которое перехватывает запрос до того, как он коснется нашей основной бизнес-логики.
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\Response;
class EnforceIdempotency
{
public function handle(Request $request, Closure $next): Response
{
// 1. We only care about requests that mutate state
if ($request->isMethodSafe()) {
return $next($request);
}
// 2. Extract the key from the headers
$idempotencyKey = $request->header('Idempotency-Key');
if (!$idempotencyKey) {
return response()->json(['error' => 'Idempotency-Key header is required for this endpoint.'], 400);
}
$cacheKey = "idempotency:{$request->user()->id}:{$idempotencyKey}";
// 3. Check if we are currently processing this exact request (Race Condition Prevention)
// We use an atomic lock to prevent two identical requests hitting the DB at the exact same millisecond
$lock = Cache::lock("idempotency_lock:{$idempotencyKey}", 10);
if (!$lock->get()) {
return response()->json(['error' => 'Request is already currently processing. Please wait.'], 409);
}
try {
// 4. Check if we have already successfully processed this request in the past
if (Cache::has($cacheKey)) {
$cachedResponse = Cache::get($cacheKey);
// Return the EXACT same response they got the first time
return response($cachedResponse['content'], $cachedResponse['status'])
->withHeaders($cachedResponse['headers']);
}
// 5. If it's a new request, let the Controller handle it
$response = $next($request);
// 6. If the response is successful, save it to Redis for 24 hours
if ($response->isSuccessful()) {
Cache::put($cacheKey, [
'content' => $response->getContent(),
'status' => $response->getStatusCode(),
'headers' => $response->headers->all(),
], now()->addHours(24));
}
return $response;
} finally {
// 7. Always release the atomic lock
$lock->release();
}
}
}
Фаза 2: Интеграция на стороне клиента
Чтобы архитектура работала, фронтенд должен ответственно генерировать ключ в момент, когда пользователь инициирует действие, а не когда отправляется сетевой запрос (чтобы гарантировать использование одного и того же ключа при повторах).
// Frontend React Example using Axios
import { v4 as uuidv4 } from 'uuid';
import axios from 'axios';
async function submitPayment(payload) {
// Generate the key once per user ACTION, not per network attempt
const idempotencyKey = uuidv4();
try {
const response = await axios.post('/api/payments', payload, {
headers: {
'Idempotency-Key': idempotencyKey
}
});
return response.data;
} catch (error) {
// If the network drops, standard Axios retry libraries can fire
// the exact same request again, and the backend will safely catch it!
console.error("Network failed, safe to retry automatically.", error);
}
}
Окупаемость инженерных затрат и атомарные блокировки
Внедрение Middleware идемпотентности кардинально меняет надежность вашего приложения. Вы полностью искореняете дублирующие списания, случайные повторные письма и призрачные записи в базе данных. Совмещая проверку кэша с атомарными блокировками кэша в Laravel ( Cache::lock() ), вы также защищаете свое приложение от состояния гонки (race conditions), когда нетерпеливый пользователь кликает по кнопке так быстро, что оба запроса долетают до сервера в одну и ту же миллисекунду до того, как первый ответ успевает закэшироваться. Этот паттерн является абсолютным, не подлежащим обсуждению стандартом для интеграции со Stripe, создания надежных публичных API и обеспечения целостности данных корпоративного уровня.
Комментарии (0)
Пока нет комментариев — будьте первым.