اتصال موجودی انبار به فروشگاه: منبع حقیقت، همزمانی و idempotency
7 دقیقه مطالعه

برای جلوگیری از overselling مرجع موجودی را مشخص کنید، کاهش موجودی را با قفل یا ایزولاسیون انجام دهید و همگامسازی را idempotent کنید.
«موجودی ۱ عدد» و دو مشتری همزمان در حال خرید: این سادهترین نمونهٔ overselling است. این مقاله سه ابزار را کنار هم میگذارد: منبع حقیقت موجودی، قفل یا ایزولاسیون در دیتابیس و idempotency برای تکرار درخواست. هر ادعا از مستندات رسمی PostgreSQL و Stripe است؛ مدل رزرو و تأخیر همگامسازی توصیهٔ نویسنده است. روی مدل خاص ERP یا انبار ایران ادعایی نداریم.
منبع حقیقت
یک سامانه باید «مرجع» موجودی باشد: یا انبار/ERP یا فروشگاه. اگر هر دو بنویسند، تعارض حتمی است. توصیهٔ نویسنده: مرجع را بهصراحت تعیین کنید و سامانهٔ دیگر فقط نمایش یا درخواست بدهد.
جلوگیری از فروش بیش از موجودی
در PostgreSQL، SELECT ... FOR UPDATE ردیفهای بازیابیشده را قفل میکند و تراکنشهای دیگر تا پایان تراکنش جاری نمیتوانند آنها را قفل، تغییر یا حذف کنند؛ آنها منتظر میمانند. پس تراکنش خرید میتواند ردیف موجودی SKU را قفل کند، مقدار را بررسی و کم کند. روش دیگر سطح Serializable است که دقیقترین ایزولاسیون است، اما مستند میگوید برنامه باید برای تکرار تراکنش پس از خطای serialization آماده باشد. کدام بهتر است به الگوی بار شما بستگی دارد و در این مقاله اندازهگیری نشده است.
رزرو و انقضا
توصیهٔ نویسنده: هنگام ورود به پرداخت، موجودی را برای مدت محدود رزرو کنید و پس از انقضا یا لغو آزاد کنید. وضعیتها را صریح نگه دارید: موجود، رزرو، فروختهشده. مدت رزرو را از رفتار واقعی پرداخت خودتان تعیین کنید، نه از عدد عمومی.

idempotency در همگامسازی
وقتی انبار و فروشگاه با API صحبت میکنند، ممکن است یک پیام دوبار برسد. Stripe توضیح میدهد کلید idempotency اجازه میدهد درخواست پس از خطای اتصال بیخطر تکرار شود؛ پاسخ اولین درخواست با آن کلید ذخیره و دوباره برگردانده میشود و اگر پارامترها فرق کنند، خطا میدهد. همین الگو را برای «کمکردن موجودی» یا «ثبت سفارش در انبار» هم میتوان به کار برد: هر عملیات شناسهٔ یکتا دارد و ثبت دوباره اثری ندارد. (کاربرد این الگو در انبار توصیهٔ نویسنده است؛ مستند Stripe دربارهٔ پرداخت است.)
تأخیر همگامسازی
اگر همگامسازی دورهای است، نمایش موجودی در فروشگاه قدیمی است. حاشیهٔ اطمینان (مثلاً فروش را وقتی موجودی کم شد محدود کنید) و آشتی دورهای میان دو سامانه را در نظر بگیرید. (توصیهٔ نویسنده.) برای نگاه معماری کلی، انتخاب معماری اپلیکیشن وب را ببینید.
قفل ردیف برای کاهش موجودی
مستند PostgreSQL دربارهٔ قفلگذاری صریح (خواندهشده در ۱۹ اکتبر ۲۰۲۶) توضیح میدهد علاوه بر قفلهای سطح جدول، قفلهای سطح ردیف وجود دارند و دو تراکنش هرگز نمیتوانند قفلهای متعارض روی یک ردیف داشته باشند؛ قفل ردیف با SELECT ... FOR UPDATE گرفته میشود و معمولاً تا پایان تراکنش نگه داشته میشود. الگوی پیشنهادی برای رزرو موجودی: در یک تراکنش، ردیف موجودی SKU را با FOR UPDATE بخوانید، بررسی کنید مقدار کافی است، سپس کم کنید. تراکنش همزمان دیگر روی همان SKU منتظر میماند و بعد مقدار بهروز را میبیند؛ یعنی فروش بیش از موجودی از همین مسیر رخ نمیدهد. جایگزین سادهتر یک UPDATE ... SET qty = qty - 1 WHERE sku = ... AND qty >= 1 و بررسی تعداد ردیفهای تغییرکرده است (پیشنهاد نویسنده؛ روی پایگاه داده واقعی آزمایش نشده).
سطح ایزولاسیون و تلاش مجدد
مستند «Transaction Isolation» میگوید در سطح Repeatable Read اگر ردیف هدف توسط تراکنش همزمان تغییر کرده و آن تراکنش commit شود، تراکنش دوم با پیام «could not serialize access due to concurrent update» لغو میشود و برنامه باید تراکنش جاری را رها کند و کل آن را از ابتدا دوباره اجرا کند؛ در بار دوم تغییر تأییدشده را میبیند. همین متن میگوید برنامههایی که از این سطح (و سطح Serializable) استفاده میکنند باید برای تلاش مجدد پس از شکست سریالسازی آماده باشند. در عمل: منطق رزرو را طوری بنویسید که اجرای دوبارهٔ کل تراکنش بیخطر باشد و تعداد تلاش مجدد را محدود کنید.
کلید idempotency برای پیامهای همگامسازی
مستند Stripe دربارهٔ درخواستهای idempotent الگویی مرجع میدهد: کلاینت یک کلید یکتا میسازد (UUID نسخهٔ ۴ یا رشتهٔ تصادفی با آنتروپی کافی، حداکثر ۲۵۵ نویسه، بدون داده حساس مثل ایمیل). سرور برای نسخهٔ ۱ API وضعیت و بدنهٔ پاسخ اولین درخواست با آن کلید را ذخیره میکند و تلاشهای مجدد همان پاسخ را میگیرند، کلیدها بعد از دستکم ۲۴ ساعت قابل حذفاند و اگر پارامترهای تلاش مجدد با اصلی فرق کند خطا برمیگردد. برای اتصال انبار میتوانید همین الگو را به کار ببرید: هر پیام «کاهش موجودی سفارش X» کلید یکتای خودش (مثلاً شناسهٔ سفارش و شمارهٔ ردیف) را دارد و دریافتکننده پیام تکراری را دوباره اعمال نمیکند. این استفاده تعمیم نویسنده از الگوست، نه توصیهٔ Stripe برای انبار.
نمونهٔ جریان رزرو (ساختگی)
- مشتری سفارش را شروع میکند؛ سامانه در تراکنش، موجودی SKU را قفل و ۱ عدد رزرو میکند و یک انقضا (مثلاً ۱۵ دقیقه؛ عدد پیشنهادی نویسنده) ثبت میکند.
- پرداخت موفق: رزرو به فروش تبدیل میشود؛ ناموفق یا منقضی: رزرو آزاد میشود.
- پیام «کاهش موجودی» با کلید idempotency به سامانهٔ انبار میرود؛ ارسال مجدد همان کلید اثر دوباره ندارد.
- گزارش آشتی روزانه موجودی فروشگاه و انبار را مقایسه میکند.
این جریان توضیح مفهومی است و برای سامانهٔ واقعی باید با حجم و شرایط کسبوکار آزمایش شود.
حالتهای خطا که باید آزموده شوند
- دو سفارش همزمان برای آخرین واحد.
- تلاش مجدد پس از قطع اتصال بدون دانستن اینکه پیام اول رسیده یا نه.
- پیام همگامسازی تکراری یا نامرتب.
- رزرو منقضیشدهای که پرداختش دیر میرسد.
برای هر مورد یک آزمون خودکار بنویسید و نتیجهٔ مورد انتظار را مستند کنید.
ملاحظات عملی پیادهسازی
- قفلها را کوتاه نگه دارید: بخش کار داخل تراکنش فقط بررسی و کاهش موجودی باشد.
- ترتیب قفلگیری روی چند SKU را ثابت کنید تا بنبست کمتر شود.
- برای همگامسازی، شمارندهٔ نسخه یا زمان آخرین تغییر ثبت کنید تا پیام قدیمیتر جایگزین جدیدتر نشود.
- لاگ هر تغییر موجودی با دلیل و شناسهٔ سفارش نگه دارید.
اینها توصیههای مهندسی نویسندهاند و در این مقاله روی بار واقعی آزمایش نشدهاند.
آزمونهای پیشنهادی همزمانی
برای اطمینان از رفتار سامانه، چند آزمون خودکار بنویسید: ده درخواست همزمان برای SKU با موجودی یک و بررسی اینکه فقط یکی موفق شود؛ ارسال دو بار یک پیام همگامسازی با کلید یکسان و بررسی اینکه موجودی یک بار تغییر کند؛ قطع اتصال بعد از commit و قبل از پاسخ و تلاش مجدد؛ انقضای رزرو همزمان با پرداخت. نتیجه را در گزارش آشتی با موجودی انبار تطبیق دهید. این آزمونها طرح پیشنهادی نویسندهاند و روی سامانهٔ واقعی اجرا نشدهاند؛ اجرای آنها در محیط آزمایشی و با داده ساختگی انجام شود.
چکلیست
- مرجع موجودی مشخص است.
- کاهش موجودی در تراکنش و با قفل یا ایزولاسیون مناسب انجام میشود.
- رزرو انقضا دارد.
- همگامسازی idempotent است و شناسهٔ یکتا دارد.
- آشتی دورهای و هشدار اختلاف تعریف شده است.
منابع و مراجع
- PostgreSQL Documentation: Explicit Locking — PostgreSQL Global Development Group.
- PostgreSQL Documentation: Transaction Isolation — PostgreSQL Global Development Group.
- Idempotent requests (Stripe API) — Stripe.
نویسنده
سا
سهیل ابراهیمپور بنیانگذار FutureForge است. روی طراحی محصول، معماری و استقرار نرمافزار سفارشی کار میکند.
یادداشتهای مرتبط
برای انتخاب مسیر فنی پروژهتان کمک میخواهید؟
از مسئله و محدودیتهایتان بگویید؛ اگر بتوانیم همراهیتان کنیم، برای گفتوگو هماهنگ میکنیم.




