Skip to content

فصل ۷ — سیستم رزرو هتل (Hotel Reservation System)

خلاصهٔ تحلیلی — این متن چکیدهٔ مفاهیم فصل به زبان فارسی است، نه ترجمهٔ واژه‌به‌واژه.

طراحی سیستم رزرو برای زنجیره‌ای مثل Marriott؛ تکنیک‌های فصل مستقیماً به Airbnb، رزرو پرواز و بلیت سینما هم قابل انتقال است.

گام ۱ — نیازمندی‌ها

  • ۵٬۰۰۰ هتل و ۱ میلیون اتاق؛ پرداخت کامل هنگام رزرو؛ لغو رزرو مجاز؛ ۱۰٪ overbooking (فروش بیش از ظرفیت به امید لغوها)؛ قیمت اتاق روزانه و پویا.
  • دامنه: صفحهٔ هتل/اتاق، رزرو، پنل ادمین، overbooking — جست‌وجوی اتاق خارج از محدوده است.
  • غیرکارکردی: همزمانی بالا (چند مشتری همزمان سراغ یک اتاق در ایام پیک) و تأخیر متوسط (چند ثانیه برای پردازش رزرو پذیرفتنی است).
  • برآورد: با اشغال ۷۰٪ و اقامت ۳ روزه ≈ ۲۴۰ هزار رزرو در روز ≈ فقط ~۳ TPS؛ با قیف ۱۰٪ تبدیل در هر گام: صفحهٔ جزئیات ~۳۰۰ QPS، صفحهٔ رزرو ~۳۰ QPS.

گام ۲ — طراحی سطح بالا

API

CRUD استاندارد REST برای هتل‌ها، اتاق‌ها و رزروها. در POST /v1/reservations فیلد reservationID نقش کلید idempotency برای جلوگیری از رزرو دوباره را دارد.

انتخاب دیتابیس — رابطه‌ای

سه دلیل: بار read-heavy است (بازدید چند مرتبه بیشتر از رزرو است و NoSQL ها برای write بهینه‌اند)؛ ACID برای رزرو حیاتی است (بدون آن جلوگیری از موجودی منفی، شارژ دوباره و رزرو دوباره دشوار است و کد برنامه پیچیده می‌شود)؛ و ساختار داده (هتل/اتاق/رزرو) روشن و پایدار و کاملاً رابطه‌ای است.

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

طرح اولیه (رزرو روی room_id) برای Airbnb درست است اما برای هتل غلط: مشتری نوع اتاق را رزرو می‌کند، نه اتاق مشخص را؛ شمارهٔ اتاق هنگام check-in تعیین می‌شود. وضعیت رزرو یک ماشین حالت ساده است: pending ← paid/canceled/rejected و paid ← refunded.

معماری میکروسرویس

کاربر/ادمین ← CDN (استاتیک‌ها) ← API gateway عمومی (و API داخلی پشت VPN برای کارکنان) ← سرویس‌ها: Hotel (دادهٔ تقریباً ایستا، cacheable)، Rate (قیمت روزانه)، Reservation (رزرو + موجودی)، Payment و Hotel Management. ارتباط بین سرویس‌ها معمولاً با gRPC.

گام ۳ — بررسی عمیق

مدل دادهٔ بهبودیافته

roomID در API به roomTypeID تبدیل می‌شود و جدول محوری room_type_inventory اضافه می‌شود با کلید مرکب (hotel_id, room_type_id, date) و ستون‌های total_inventory (منهای اتاق‌های در تعمیر) و total_reserved. یک ردیف به ازای هر روز، کوئری بازهٔ تاریخ را ساده می‌کند؛ ردیف‌های دو سال آینده با job روزانه از پیش پر می‌شوند. حجم: ۵٬۰۰۰ × ۲۰ نوع اتاق × ۷۳۰ روز ≈ ۷۳ میلیون ردیف — در یک دیتابیس جا می‌شود؛ replication برای HA.

بررسی موجودیت رزرو: انتخاب ردیف‌های بازهٔ تاریخ و برای هر روز شرط total_reserved + N <= total_inventory؛ پیاده‌سازی overbooking فقط تغییر شرط به <= 110% * total_inventory است.

اگر داده بزرگ شد: آرشیو رزروهای گذشته به cold storage و شاردینگ با hotel_id (چون تقریباً همهٔ کوئری‌ها اول هتل را انتخاب می‌کنند).

مسئلهٔ همزمانی — قلب فصل

سناریو ۱: دوبار کلیک همان کاربر. غیرفعال‌کردن دکمه سمت کلاینت کافی نیست (جاوااسکریپت قابل دور زدن است). راه‌حل: API idempotentreservation_id که قبل از پرداخت تولید شده، کلید اصلی جدول رزرو است؛ درج دوم با نقض unique constraint شکست می‌خورد.

سناریو ۲: دو کاربر و یک اتاق آخر. با سطح ایزولاسیون غیر serializable، هر دو تراکنش شرط موجودی را True می‌بینند (تغییرات یکی برای دیگری تا commit نامرئی است) و هر دو رزرو می‌کنند. سه راه‌حل:

روشسازوکارمزایامعایب
قفل بدبینانهSELECT ... FOR UPDATE؛ تراکنش دوم منتظر می‌ماندساده؛ مناسب contention سنگینخطر deadlock؛ قفل طولانی مقیاس‌پذیری را می‌کشد
قفل خوش‌بینانهستون version؛ نوشتن فقط اگر version جلو نرفته باشدبدون قفل دیتابیس؛ سریع در contention کمدر contention بالا موج retry و تجربهٔ بد
قید دیتابیسCHECK(total_inventory - total_reserved >= 0)ساده‌ترین پیاده‌سازیشکست‌های گیج‌کننده در contention بالا؛ قید version-control نمی‌شود؛ همهٔ دیتابیس‌ها پشتیبانی نمی‌کنند

چون QPS رزرو پایین است، قفل خوش‌بینانه یا قید دیتابیس توصیه می‌شود؛ قفل بدبینانه نه.

مقیاس‌پذیری (سناریوی booking.com — ۱۰۰۰ برابر QPS)

  • سرویس‌ها stateless اند و راحت مقیاس می‌گیرند؛ گلوگاه دیتابیس است ← شاردینگ با hotel_id (مثلاً ۳۰ هزار QPS بین ۱۶ شارد ≈ ~۱٬۹۰۰ به ازای هر شارد).
  • کش موجودی (Redis): فقط دادهٔ حال و آینده معناست ← TTL و LRU طبیعی‌اند. کلید hotelID_roomTypeID_date، مقدار تعداد اتاق خالی. کوئری‌های موجودی به کش می‌روند و اکثر درخواست‌های ناموفق پشت کش می‌مانند؛ دیتابیس اول به‌روزرسانی می‌شود و تغییر ناهمگام (کد برنامه یا CDC با Debezium) به کش می‌رسد.
  • نکتهٔ ظریف: ناسازگاری کش-دیتابیس مهم نیست به شرطی که دیتابیس اعتبارسنجی نهایی را انجام دهد — کش می‌گوید اتاق هست، دیتابیس رد می‌کند، کاربر خطای «اتاق تمام شد» می‌بیند. دیتابیس source of truth می‌ماند.

سازگاری داده بین میکروسرویس‌ها

طراحی ما عمداً هیبریدی است: رزرو و موجودی در همان سرویس و همان دیتابیس رابطه‌ای، تا ACID مسائل همزمانی را حل کند. در میکروسرویس خالص (هر سرویس دیتابیس خودش) یک عملیات منطقاً اتمیک چند سرویس را در بر می‌گیرد و transaction واحد ممکن نیست؛ راه‌حل‌ها: 2PC (اتمیک اما blocking و کند) و Saga (زنجیرهٔ تراکنش‌های محلی + تراکنش‌های جبرانی؛ سازگاری نهایی). داوری معمار: این پیچیدگی اینجا ارزشش را ندارد.

نکات کلیدی فصل

  • «چه چیزی واقعاً رزرو می‌شود؟» (نوع اتاق، نه اتاق) — فهم دامنه، اسکیما را تعیین می‌کند و اشتباه در آن کل طراحی را خراب می‌کند.
  • دفاع دو لایه در برابر رزرو دوباره: کلید idempotency برای دوبار کلیک، و کنترل همزمانی (خوش‌بینانه/قید) برای رقابت کاربران.
  • انتخاب مکانیزم قفل تابع میزان contention است، نه سلیقه.
  • کشِ جلوی دیتابیس وقتی امن است که دیتابیس حرف آخر را بزند.
  • عمل‌گرایی در برابر خلوص میکروسرویس: داده‌هایی که با هم تراکنش می‌خواهند، کنار هم بمانند.

یادداشت‌های مترجم

  • idempotency، overbooking، optimistic/pessimistic locking و Saga در متن لاتین/فارسی ترکیبی آمده‌اند.