یکی از بدترین تجربه‌های یک فروشگاه اینترنتی این است: مشتری پول داده، پیامک تأیید گرفته، و فردا با او تماس می‌گیرید که «متأسفانه موجودی نداشتیم».

این باگ در تست دیده نمی‌شود. در محیط توسعه، شما یک درخواست در هر لحظه دارید و همه‌چیز درست کار می‌کند. مسئله فقط وقتی ظاهر می‌شود که دو نفر در یک ثانیه آخرین کالا را بخرند، و آن ثانیه معمولاً وسط یک کمپین است.

چرا کنترل در کد جواب نمی‌دهد

منطق ساده به نظر می‌رسد: موجودی را بخوان، اگر کافی بود کم کن، سفارش را ثبت کن.

مسئله در فاصله‌ی بین «خواندن» و «کم کردن» است. دو درخواست هم‌زمان هر دو موجودی را قبل از کم شدن می‌خوانند، هر دو عدد یک را می‌بینند، و هر دو جلو می‌روند. حالا دو سفارش دارید و یک کالا.

این با قفل در حافظه‌ی برنامه هم حل نمی‌شود، چون معمولاً بیش از یک نسخه از برنامه در حال اجراست. قفلی که فقط در یک نسخه اعتبار دارد، نسخه‌ی دوم را نمی‌بیند.

راه‌حل: قفل در پایگاه داده

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

نتیجه‌اش این است که درخواست دوم پشت درخواست اول منتظر می‌ماند، و وقتی نوبتش شد، موجودی به‌روزشده را می‌بیند و درست رد می‌شود.

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

رزرو موجودی، و چرا باید مهلت داشته باشد

وقتی سفارش ثبت شد ولی هنوز پرداخت نشده، موجودی در وضعیت مبهمی است: نه فروخته شده، نه آزاد است.

راه درست رزرو است: موجودی به آن سفارش اختصاص داده می‌شود و از دسترس بقیه خارج می‌شود. ولی رزرو بدون مهلت، خودش یک مسئله‌ی تازه می‌سازد، مشتری که پرداخت را رها کرده، کالا را برای همیشه قفل کرده است.

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

وقتی موجودی یک عدد ساده نیست

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

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

چطور تست کنید

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

تست با پایگاه داده‌ی شبیه‌سازی‌شده در حافظه این را نشان نمی‌دهد، چون آن‌ها ذاتاً درخواست‌ها را پشت سر هم اجرا می‌کنند. اگر تستتان همیشه سبز است، شاید فقط دارد چیز اشتباهی را می‌سنجد.