طراحی و پیاده سازی

Idempotency در API چیست و چرا در تراکنش‌های حساس مهم است؟

دکمه پرداخت را می‌زنید؛ چرخ بارگذاری کمی می‌چرخد، شبکه برای لحظه‌ای قطع می‌شود و پیام خطای ارتباط ظاهر می‌شود. محتاطانه دوباره روی همان دکمه می‌زنید. پرسشی که حالا برنامه‌نویس، مدیر محصول و مشتری هر سه باید به آن فکر کنند: پول یک بار کم شد یا دو بار؟ سامانه‌ای که برای این پرسش پاسخ مطمئن دارد، به مفهومی تکیه کرده است به نام Idempotency.

در سامانه‌های واقعی، تکرار درخواست استثنا نیست؛ حالت عادی است. شبکه قطع می‌شود، کاربر دوباره کلیک می‌کند، اپ تلاش مجدد (Retry) خودکار انجام می‌دهد و پیام‌ها گاهی دو بار می‌رسند. در این مقاله با یک سناریوی فروش بلیت جلو می‌رویم، مفهوم عملیات Idempotent را باز می‌کنیم و نشان می‌دهیم چرا در پرداخت، ثبت سفارش و عملیات حساس، این مفهوم مرز میان اشتباه کوچک و بحران است.

سناریو: بلیتی که نباید دوبار فروخته می‌شد

فرض کنید سامانه فروش بلیت یک رویداد را راه‌اندازی کرده‌اید. مشتری پرداخت را تأیید می‌کند، پاسخ دروازه پرداخت در راه است که شبکه قطع می‌شود. اپ طبق طراحی، درخواست را دوباره می‌فرستد. اگر سرور برای هر «ثبت خرید» یک بلیت تازه بسازد، نتیجه دو بلیت و دو برداشت است — با اینکه مشتری فقط یک بار خواسته است. حالا تصور کنید همین درخواست دوباره، روزی هزاران بار به دلایل مختلف رخ می‌دهد: این دیگر حادثه نیست، الگوی رفتار شبکه است.

تحلیل: تکرار از کجا می‌آید و چرا اجتناب‌ناپذیر است؟

منشأ تکرارها معمولاً سه چیز است. نخست ابهام تایم‌اوت: وقتی پاسخ دیر می‌رسد، فرستنده نمی‌داند عملیات انجام شده یا نه و رفتار منطقی، تلاش مجدد است. دوم رفتار طبیعی کاربران با رابط‌های کند: کلیک دوم و سوم. سوم معماری‌های توزیع‌شده که در آن‌ها پیام‌ها با تضمین «حداقل یک‌بار» جابه‌جا می‌شوند؛ یعنی تکرار، وعده سیستم است نه خطای آن.

نکته کلیدی این است: شکستِ درخواست همیشه یعنی شکست عملیات نیست. ممکن است پرداخت در سرور موفق شده باشد اما پاسخ در راه گم شود. پس مشکل، خودِ تکرار نیست؛ مشکل عملیاتی است که با هر تکرار، اثر تازه‌ای می‌سازد.

مفهوم: عملیات Idempotent یعنی چه؟

عملیات Idempotent (توان‌بی‌اثر) عملیاتی است که اجرای چندباره‌اش با همان ورودی، به همان نتیجه می‌رسد و اثر اضافه‌ای نمی‌گذارد. در زندگی روزمره، روشن‌کردن کلید برق چنین است: صد بار فشار دهید، چراغ یا روشن است یا خاموش؛ وضعیت نهایی یکی است. اما فشردن دکمه «یک واحد به سبد اضافه کن» چنین نیست: هر فشار یکی اضافه می‌کند.

عملیاتماهیترفتار با اجرای مجددنمونه
GET (خواندن)ذاتاً Idempotentفقط داده را می‌خواند؛ تکرار اثر نداردمشاهده وضعیت سفارش
PUT (جایگزینی کامل)ذاتاً Idempotentوضعیت نهایی همان مقدار اعلام‌شده می‌ماندثبت کامل اطلاعات پیک
DELETE (حذف منبع مشخص)ذاتاً Idempotentمنبع حذف می‌شود؛ تکرار اثر تازه‌ای نداردلغو رزرو با شناسه مشخص
POST بدون سازوکار محافظتمعمولاً غیر Idempotentهر اجرا ممکن است منبع تازه بسازدثبت خرید جدید
POST با کلید یکتاعملاً Idempotentسرور تکرار را می‌شناسد و پاسخ نخست را برمی‌گرداندثبت خرید با کلید Idempotency

راهکار: کلید یکتای Idempotency

رایج‌ترین راهکار ساده است: فرستنده پیش از نخستین تلاش، یک شناسه یکتا برای خودِ «عملیات» می‌سازد و آن را همراه درخواست می‌فرستد. سرور این شناسه را ثبت می‌کند و اگر همان شناسه دوباره رسید، به‌جای اجرای دوباره، پاسخ ذخیره‌شده نخستین اجرا را برمی‌گرداند. نتیجه: تکرار بی‌خطر می‌شود و اپ می‌تواند با خیال راحت تلاش مجدد کند، بدون آنکه مشتری جریمه شود.

سازوکارهای کمکی هم کنار کلید یکتا به کار می‌آیند: کلید طبیعی کسب‌وکار مثل شماره سفارش یا شماره پیگیری که خودش تکرارناپذیر است؛ قید یکتایی در پایگاه داده که ساخت رکورد دوباره با همان شناسه را ممکن نمی‌کند؛ و بازه اعتبار برای کلیدها تا انبار شناسه‌ها بی‌نهایت بزرگ نشود. ترکیب این سه، دفاع چندلایه‌ای می‌سازد که هر لایه، خلأ لایه قبلی را پر می‌کند.

کاربرد در عملیات حساس

پرداخت

در دروازه‌های پرداخت، کلید یکتا یعنی تفکیک «تلاش» از «تراکنش». هر تلاش پرداخت یک شناسه دارد؛ اگر شبکه قطع شود و اپ همان تلاش را با همان شناسه دوباره بفرستد، درگاه پاسخ نخست را می‌دهد و پول یک بار جابه‌جا می‌شود. بدون این مکانیزم، هر تلاش مجدد پنجره‌ای برای برداشت دوم است و اعتماد کاربر، قربانی نخستین قطعی شبکه می‌شود.

ثبت سفارش

در فروش بلیت، سامانه به‌جای قبول هر درخواست خام، روی «شناسه تلاش خرید» سخت‌گیری می‌کند: دو درخواست با همان شناسه، یک بلیت؛ دو شناسه متفاوت، دو خرید مستقل. به این ترتیب نه فروش واقعی از دست می‌رود و نه مشتری به‌خاطر خطای شبکه، دو بلیت و دو هزینه می‌گیرد. همین قاعده در ثبت سفارش فروشگاه‌ها و ثبت درخواست‌های خدماتی هم صادق است.

عملیات حساس دیگر

انتقال وجه میان حساب‌ها، صدور گواهی، ابطال و استرداد و اجرای دستورهای عملیاتی روی زیرساخت، همه خانواده‌ای هستند که تکرارشان پرهزینه است. قاعده مشترک ساده است: هر عملیاتی که اثر مالی یا حقوقی دارد، باید یا ذاتاً Idempotent باشد یا با کلید یکتا به رفتار Idempotent تبدیل شود.

خطاهای رایج پیاده‌سازی

  • تولید شناسه تازه در هر تلاش مجدد: با این کار هر تکرار یک عملیات جدید حساب می‌شود و مکانیزم عملاً بی‌اثر است؛ شناسه باید به «عملیات» تعلق داشته باشد، نه به «ارسال».
  • پاسخ خطا به‌جای پاسخ ذخیره‌شده برای درخواست تکراری: اگر سرور تکرار را شناسایی کند اما به‌جای نتیجه نخست خطای عمومی بدهد، فرستنده باز هم مجبور به تصمیم‌گیری کور می‌شود.
  • اکتفا به غیرفعال‌کردن دکمه در رابط کاربری: تلاش مجدد می‌تواند از اپ، شبکه یا سرویس واسط بیاید؛ تضمین باید در سرور و داده باشد.
  • کلیدهای بی‌انتها بدون بازه اعتبار: انبار شناسه‌ها مدام بزرگ می‌شود و نگهداری و جست‌وجو هزینه‌ساز می‌شود.

کاربرد عملی

در طراحی API خود، برای هر عملیات نوشتن این سه پرسش را کنار هم بگذارید: اگر این درخواست دو بار برسد چه می‌شود؟ تکرار با چه چیزی شناسایی می‌شود؟ و پاسخ تکرار، همان پاسخ نخست است یا چیزی مبهم؟ پاسخ‌های روشن به این سه پرسش، به معنای قرارداد شدن Idempotency میان شما و همه پذیرنده‌های API است؛ چیزی که در مستندات سرویس‌های پرداخت و پیام‌رسانی حرفه‌ای هم به‌روشنی به آن اشاره می‌شود.

درس‌آموخته

سناریوی بلیت یک درس دارد: در سامانه‌های توزیع‌شده، طراحی برای تلاش مجدد بخشی از طراحی اصلی است، نه پیوست احتیاطی. شبکه‌ها ناپایدارند، کاربران مضطرب کلیک می‌کنند و پیام‌ها تکرار می‌شوند؛ سامانه‌ای که این واقعیت را نپذیرفته باشد، در نخستین قطعی جدی، اعتماد و پول را با هم از دست می‌دهد. هر سه پرسش طراحی را روشن کنید تا تراکنش‌های شما به اندازه وعده‌هایتان قابل اعتماد باشند.

نکات کلیدی این مقاله

  • تکرار درخواست در سامانه‌های واقعی عادی است؛ شبکه، کاربر و معماری توزیع‌شده همه به نفع آن دست می‌زنند.
  • عملیات Idempotent با هر تکرار اثر تازه نمی‌سازد؛ GET، PUT و DELETE ذاتاً چنین‌اند و POST معمولاً نیست.
  • کلید یکتای Idempotency تلاش را از تراکنش جدا می‌کند: تکرار شناسایی و پاسخ نخست بازگردانده می‌شود.
  • در پرداخت و ثبت سفارش، کلید یکتا مرز میان برداشت یک‌باره و برداشت دوم است.
  • غیرفعال‌کردن دکمه در ظاهر، محافظت واقعی نیست؛ تضمین باید سمت سرور و در داده باشد.

سوالات متداول

آیا عملیات Idempotent یعنی بدون تغییر است؟

نه. این عملیات می‌تواند وضعیت را تغییر دهد، اما نکته این است که تکرارش اثر اضافه نمی‌سازد: تغییر در اجرای نخست رخ می‌دهد و اجراهای بعدی، همان وضعیت نهایی را حفظ می‌کنند.

کلید Idempotency را چه کسی تولید می‌کند؟

معمولاً فرستنده — اپ یا سرویس واسط — پیش از نخستین تلاش، شناسه یکتایی برای خودِ عملیات می‌سازد و در همه تلاش‌های مجدد همان را می‌فرستد. تولید شناسه در سرور هم ممکن است، اما تضمین ثبات آن در تکرارها به‌عهده فرستنده است.

برای عملیات‌های خواندن هم به کلید یکتا نیاز داریم؟

معمولاً نه؛ خواندن ذاتاً Idempotent است و تکرارش اثر جانبی ندارد. کلید یکتا جایی لازم می‌شود که عملیات اثر می‌نویسد و تکرارش پرهزینه است: پرداخت، ثبت، حذف یا انتقال.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا