Idempotency Key چیست؟ راه جلوگیری از درخواست و تراکنش تکراری در API
با Idempotency Key از اجرای تکراری سفارش یا پرداخت هنگام Retry جلوگیری کنید؛ نحوه کار، ذخیره کلید، وبهوک و خطاهای رایج را یاد بگیرید.
کاربر روی دکمه «ثبت سفارش» میزند. سرور درخواست را دریافت میکند و سفارش در دیتابیس ساخته میشود، اما درست هنگام ارسال پاسخ اینترنت قطع میشود. برای کاربر فقط یک پیام خطا باقی مانده است. دوباره روی دکمه میزند؛ حالا سیستم باید چه کند؟ اگر درخواست دوم را بیفکر اجرا کند، شاید دو سفارش ساخته شود. اگر همه درخواستهای تکراری را رد کند، ممکن است کاربر هرگز نتیجه عملیات اول را نفهمد.
اینجا Idempotency Key به کار میآید: یک شناسه برای تشخیص تلاشهای مجددِ همان عملیات. این الگو در پرداخت، ثبت سفارش و APIهای عملیاتی استفاده میشود و ارزش واقعی آن وقتی مشخص میشود که ارتباط شبکه درست در بدترین لحظه قطع شده باشد.
مفهوم Idempotency به زبان ساده
در نرمافزار، عملیاتی را «ایدِمپوتِنت» یا تکرارپذیرِ بدون اثر اضافه مینامیم که اجرای چندباره آن، اثر نهایی بیشتری از یک بار اجرا نگذارد. مثلاً اگر تنظیم کنید وضعیت اعلانهای یک حساب «خاموش» باشد، تکرار همین تنظیم نباید هر بار یک حساب تازه بسازد.
اما درخواست ساخت سفارش معمولاً چنین رفتاری ندارد. ارسال دوباره POST /orders میتواند سفارش دوم ایجاد کند. Idempotency Key به سرویس کمک میکند بفهمد دو درخواست ظاهراً جدا، در واقع دو بار تلاش برای یک تصمیم کاربر هستند.
این کلید رمز عبور یا مجوز دسترسی نیست. حتی با داشتن کلید، سرور باید احراز هویت و بررسی مالکیت منابع را انجام دهد. بهتر است آن را برچسب پیگیری یک عملیات بدانید، نه ابزار امنیتی مستقل.
مشکلی که Retry معمولی حل نمیکند
در سامانههای واقعی، شکست درخواست لزوماً به معنی شکست عملیات نیست. گاهی اتصال قبل از رسیدن درخواست قطع میشود؛ گاهی دیتابیس کارش را انجام داده، اما پاسخ به کلاینت نمیرسد. از روی Timeout بهتنهایی نمیتوان فهمید کدام حالت رخ داده است.
فرض کنید مشتری سبد خریدی با دو محصول دارد. درخواست ثبت سفارش فرستاده میشود و پاسخ پس از چند ثانیه نامعلوم میماند. برنامه موبایل تصمیم میگیرد Retry کند. اگر هر بار شناسه تازه بسازد، سرور ممکن است دو عملیات مستقل ببیند. در مقابل، اگر همان کلید قبلی را بفرستد و سرور از آن پشتیبانی کند، میتوان نتیجه عملیات اول را پیدا کرد یا وضعیت آن را با همان شناسه پیگیری کرد.
پس راهکار درست صرفاً «اگر خطا آمد دوباره بفرست» نیست؛ باید مشخص کنیم کدام خطا قابل تلاش مجدد است و هنگام تلاش مجدد، هویت همان درخواست چگونه حفظ میشود.
Idempotency Key چگونه کار میکند؟
یک گردشکار ساده را تصور کنید:
- کلاینت پیش از ثبت سفارش، یک شناسه تصادفی یکتا تولید میکند.
- درخواست ساخت سفارش همراه سربرگ
Idempotency-Keyارسال میشود. - سرور ابتدا بررسی میکند آیا در محدوده همان کاربر و عملیات، این کلید قبلاً ثبت شده است یا نه.
- در صورت جدید بودن، اجرای امن عملیات آغاز میشود و وضعیت درخواست ذخیره میشود.
- اگر همان درخواست دوباره برسد، سرور به جای ساخت سفارش دوم، نتیجه قبلی یا وضعیت در حال پردازش را برمیگرداند.
نمونه آموزشی درخواست به این شکل است. نشانی سایت و شناسه صرفاً مثالاند و قرار نیست آن را بدون پیادهسازی سمت سرور اجرا کنید:
POST /api/orders HTTP/1.1Host: api.example.comAuthorization: Bearer <ACCESS_TOKEN>Content-Type: application/jsonIdempotency-Key: 1b6e4f50-8a47-4ee6-b188-c4a3c9f0d902{"cart_id":"cart_842","delivery":"standard"}از دید سرور، کلید فقط یکی از ورودیهای تصمیم است. سرویس باید بداند این عملیات متعلق به کدام حساب و کدام endpoint بوده و داده درخواست با تلاش قبلی یکسان است یا نه. اگر کلیدی قبلاً برای یک سبد خرید استفاده شده، نباید اجازه دهیم با همان کلید و مبلغ یا محتوای متفاوت عملیات جدید انجام شود.
تفاوت Retry، Idempotency و Exactly Once
این سه مفهوم در گفتگوهای فنی گاهی قاطی میشوند. Retry یعنی درخواست را بعد از شکست دوباره تلاش کنیم. Idempotency کمک میکند تکرارِ همان عملیات، اثر اضافه نداشته باشد. اما Exactly Once ادعای بزرگتری درباره اجرای دقیقاً یکباره در یک سامانه توزیعشده است و به هماهنگی اجزا وابسته میشود.
یک مثال مهم: شاید API سفارش فقط یک رکورد بسازد، ولی همزمان برای سیستم انبار پیام بفرستد و از درگاه پرداخت هم استفاده کند. کلید در لایه API بهتنهایی تضمین نمیکند همه این اثرهای جانبی فقط یک بار اتفاق بیفتند. برای اجزای دیگر نیز به شناسه پایدار عملیات، ثبت اتمیک وضعیت یا الگوهایی مانند Transactional Outbox و پردازش تکرارپذیر پیام نیاز دارید.
مفهوم | چه کاری انجام میدهد؟ | چه چیزی را تضمین نمیکند؟ |
|---|---|---|
Retry | تلاش دوباره بعد از شکست یا Timeout | جلوگیری از اجرای تکراری |
Idempotency Key | تشخیص تلاشهای یک عملیات و کنترل اثر تکرار | یکبار اجرا شدن تمام سرویسهای جانبی |
شناسه یکتای سفارش | جلوگیری از ایجاد دو سفارش با یک شناسه تجاری | بازیابی خودکار پاسخ شبکه |
قید UNIQUE دیتابیس | جلوگیری از ثبت رکورد تکراری در محدوده تعریفشده | هماهنگی تمام عملیات خارجی |
سمت سرور چه چیزهایی باید ذخیره شود؟
پیادهسازی قابل اتکا معمولاً به رکوردی برای پیگیری درخواست نیاز دارد. فیلدهای متداول آن عبارتاند از شناسه کلید، محدوده مالکیت، اثر انگشت داده ورودی، وضعیت پردازش، زمان ثبت و نتیجه عملیات. طراحی دقیق به نوع محصول بستگی دارد؛ لازم نیست همیشه بدنه کامل پاسخ برای همیشه در دیتابیس بماند.
بهتر است وضعیتها روشن باشند: processing برای کار در جریان، completed برای عملیات نهاییشده و در صورت نیاز حالت خطایی که اجازه تلاش دوباره را مشخص میکند. نکته حساس، رقابت دو درخواست همزمان با یک کلید است. اگر هر دو قبل از ثبت وضعیت از سد بررسی عبور کنند، ساخت سفارش تکراری همچنان ممکن است.
برای مهار این رقابت از قید یکتا در پایگاهداده و عملیات اتمیک استفاده میشود. صرف اجرای یک کوئری SELECT و سپس INSERT بدون کنترل همزمانی کافی نیست. اگر سرویس روی چند سرور اجرا شود، نگهداری کلید فقط در حافظه یک نمونه نیز قابل اتکا نخواهد بود.
اگر همان کلید با داده متفاوت ارسال شود چه؟
این مورد باید بهعنوان استفاده نادرست از کلید شناخته شود، نه یک سفارش تازه. راه رایج، مقایسه بخشی پایدار از داده درخواست یا Fingerprint آن است. پاسخ خطا میتواند توضیح دهد که کلید قبلاً با ورودی دیگری ثبت شده است؛ کد HTTP دقیق را قرارداد همان API تعیین میکند.
برای مثال، اگر کاربر ابتدا یک سفارش با روش ارسال معمولی ثبت کرد و بعد واقعاً تصمیم گرفت ارسال فوری بخواهد، این تغییر نباید با ارسال دوباره همان کلید و بدنه جدید پنهان شود. برنامه باید از endpoint و فرایند مناسب برای تغییر سفارش استفاده کند یا عملیات مستقل تازهای تعریف کند.
به همین دلیل تولید کلید باید به نیت عملیاتی کاربر وابسته باشد: تلاش مجدد همان عملیات، همان کلید؛ شروع یک عملیات متفاوت، کلید جدید.
عمر Idempotency Key چقدر است؟
هیچ مدت جهانی برای همه سرویسها وجود ندارد. مدت نگهداری را مالک API تعیین میکند و باید آن را در مستندات روشن بنویسد. بهعنوان نمونه، در مدل قدیمیتر API Stripe، کلیدها پس از حداقل ۲۴ ساعت میتوانند پاک شوند. این عدد برای همه نسخههای Stripe یا سرویسهای دیگر یک قانون عمومی نیست.
این جزئیات بسیار مهم است: پس از حذف کلید منقضیشده، استفاده دوباره از همان مقدار ممکن است بهعنوان عملیات تازه تعبیر شود. پس اگر ثبت تراکنش مالی دارید، نباید تنها به عمر کوتاه cache تکیه کنید. شناسه تجاری یکتا، وضعیت سفارش و کنترل صحت پرداخت باید جداگانه وجود داشته باشند.
مدت نگهداری معمولاً به پنجره Retry کلاینت، زمان اجرای عملیات و نیازهای رسیدگی به خطا بستگی دارد. رکوردهای قدیمی را نیز نباید بدون برنامه پاک کرد، چون ممکن است تحقیقات پشتیبانی یا بررسی مالی را دشوار کنند.
نمونه Stripe چه چیزی به ما یاد میدهد؟
Stripe یکی از مثالهای شناختهشده استفاده از کلید تکرارپذیری است. در مستندات API نسخه اول آن، برای درخواستهای سازگار با این الگو میتوان کلیدی یکتا فرستاد تا تلاش مجدد همان درخواست بهطور معمول عملیات تازهای نسازد. رفتار ذخیره پاسخ، پردازش همزمان و مدت نگهداری به نسخه و نوع API وابسته است؛ بنابراین نباید سیاست یک ارائهدهنده را بدون بررسی به سامانه خود منتقل کنیم.
مثلاً ذخیره و برگرداندن همان پاسخ ناموفق در برخی پیادهسازیها تصمیم طراحی آگاهانه است. سرویس دیگری ممکن است برای خطاهای پیش از شروع پردازش اجازه Retry بدهد. سؤال درست این نیست که «کدام سرویس دقیقاً چه عددی دارد؟» بلکه این است که قرارداد API شما درباره نتیجه نهایی، پردازش همزمان و کلید منقضیشده چه میگوید.
در وبهوکها نیز به Idempotency نیاز داریم؟
بله، اما معمولاً شناسه خود رویداد نقش اصلی را بازی میکند. سرویسهای ارسالکننده وبهوک ممکن است به دلیل Timeout یا عدم دریافت پاسخ موفق، رویدادی را دوباره ارسال کنند. مصرفکننده باید رویداد تکراری را تشخیص دهد و از ثبت دوباره اثر تجاری جلوگیری کند.
در اینجا یک تفاوت ظریف وجود دارد: کلید درخواست کاربر هنگام ساخت سفارش، لزوماً همان شناسه رویداد تحویلشده از سیستم پرداخت نیست. برای مثال، عملیات order_created و پیام payment_succeeded هرکدام هویت جدا دارند. حتی اگر هر پیام چند بار برسد، پردازش آن باید برای همان رویداد کنترلشده باشد.
در کنار تشخیص رویداد تکراری، اصالت پیام وبهوک را نیز طبق سازوکار امضای ارائهدهنده بررسی کنید. یک Event ID بدون اعتبارسنجی فرستنده، بهتنهایی قابل اعتماد نیست.
پنج اشتباه رایج در پیادهسازی
- ساخت کلید جدید برای هر Retry: عملاً تلاش دوم را مستقل نشان میدهد.
- یکسان دانستن کلید بین همه کاربران: ممکن است وضعیت یک حساب با حساب دیگر تداخل پیدا کند. محدوده کلید باید روشن باشد.
- اتکا به حافظه محلی یک سرور: در چند نمونه همزمان یا پس از راهاندازی مجدد میتواند رکورد تکرار را از دست بدهد.
- نادیدهگرفتن درخواستهای همزمان: دو درخواست با یک کلید ممکن است همزمان به مرحله ساخت برسند؛ کنترل اتمیک لازم است.
- فرض اینکه کلید جای مجوز را میگیرد: دسترسی کاربر به سفارش، احراز هویت و اعتبار داده باید همچنان جدا بررسی شوند.
اشتباه دیگری هم هست که معمولاً دیر خود را نشان میدهد: حذف رکورد Idempotency قبل از پایان عمر واقعی عملیات. اگر کاری در صف طولانی اجرا میشود، رکورد پیگیری نباید زودتر از امکان تکمیل یا Retry آن ناپدید شود.
چطور این قابلیت را قبل از انتشار آزمایش کنیم؟
اول یک سفارش آزمایشی بسازید و همان درخواست را با کلید یکسان دوباره بفرستید. باید روشن باشد آیا پاسخ قبلی بازگردانده میشود یا پیام وضعیت. سپس همان کلید را با داده متفاوت امتحان کنید؛ API نباید بدون توضیح آن را عملیات تازه بداند.
آزمایش مهمتر، دو درخواست همزمان با یک کلید است. این آزمون بهتر است در محیطی انجام شود که به تعداد نمونههای واقعی سرویس نزدیک باشد. بعد اتصال را میان ثبت دیتابیس و دریافت پاسخ قطع کنید و ببینید آیا کلاینت با Retry میتواند نتیجه درست را بازیابی کند.
در پایان، حالتهای دشوار را هم فراموش نکنید: انقضای کلید، شکست پردازش جانبی، تأخیر صف، تغییر حساب کاربر و تکرار وبهوک. موفقیت یک تست ساده با دو درخواست پشتسرهم به معنی آماده بودن سامانه برای تراکنش واقعی نیست.
پرسشهای متداول درباره Idempotency Key
آیا Idempotency Key فقط مخصوص پرداخت است؟
خیر. ساخت سفارش، ایجاد اشتراک، ثبت درخواست پشتیبانی یا هر عملیات تغییردهنده وضعیت که احتمال Retry دارد میتواند از این الگو بهره ببرد.
آیا خود POST بهطور پیشفرض Idempotent است؟
خیر. متد POST بهصورت عمومی چنین تضمینی ندارد. فقط وقتی API مشخصی مکانیزم Idempotency را پیاده کرده و مستند کرده باشد میتوانید روی رفتار آن تکیه کنید.
از UUID برای کلید استفاده کنیم؟
UUID تصادفی مانند نسخه ۴ انتخاب رایجی است، اما الزامات دقیق کلید را سرویس مقصد تعیین میکند. داده حساس یا اطلاعات قابل حدس کاربر را داخل کلید نگذارید.
آیا داشتن Idempotency Key جلوی همه پرداختهای دوباره را میگیرد؟
خیر. رفتار درگاه، زمان نگهداری کلید و هماهنگی اجزای دیگر مهم است. شناسه تجاری یکتا و تطبیق وضعیت نهایی پرداخت همچنان ضروری هستند.
آیا لازم است کلید را در لاگ ثبت کنیم؟
ثبت یک شناسه پیگیری میتواند عیبیابی را سادهتر کند، اما اطلاعات حساس یا توکنهای احراز هویت را در لاگ ننویسید. مدت نگهداری و سطح دسترسی به لاگ نیز باید محدود باشد.
جمعبندی: اعتمادپذیری در لحظههای نامطمئن ساخته میشود
بیشتر روزها درخواست API یک بار فرستاده میشود و پاسخ برمیگردد؛ همه چیز ساده به نظر میرسد. ارزش Idempotency Key در روزی آشکار میشود که پاسخ گم میشود، دو Worker همزمان کار میکنند یا وبهوک دوباره میرسد. اگر کلید را با ثبت وضعیت اتمیک، کنترل مجوز و شناسه تجاری پایدار همراه کنید، سیستم برای این لحظهها آمادگی بیشتری خواهد داشت. هدف، حذف همه خطاها نیست؛ هدف این است که یک خطای شبکه به دو سفارش یا دو عملیات ناخواسته تبدیل نشود.
نظرات و گفتوگو
نظرها پس از تایید مدیر منتشر میشوند. پاسخهای رسمی مدیر با نشان مشخص نمایش داده میشوند.