Business Systems১২ মিনিট পাঠ

টিকেট বুকিং সিস্টেম (Ticket Booking System)

বিলিয়ন বিলিয়ন মানুষের জন্য হাই কনকারেন্সি (high concurrency) বা একইসাথে অনেক ইউজারের টিকিট বুকিং হ্যান্ডেল করা এবং ডাবল বুকিং বা দ্বিগুণ বিক্রি হওয়া রোধ করা
scope:রিয়েল-ওয়ার্ল্ড সিস্টেম (Real-World System)difficulty:অ্যাডভান্সড (Advanced)

সমস্যাটি বোঝা (Understanding the Problem)

একটি মুভি, কনসার্ট বা ফ্লাইটের টিকিট কাটার প্ল্যাটফর্ম যেমন BookMyShow, Ticketmaster বা সহজ (Shohoz)-এর মতো একটি সিস্টেম বুকিং ডিজাইন করতে হবে। এটি আপাতদৃষ্টিতে বেশ সহজ মনে হলেও, এটি আসলে হাই কনকারেন্সি (high concurrency) বা বিশাল স্কেলে ইউজারের একইসাথে অ্যাক্সেস রিড-রাইট বা এক্সেস সামলানোর একটি ক্লাসিক বা চমৎকার সমস্যা।

ফাংশনাল প্রয়োজনীয়তা (Functional Requirements):

  • ইভেন্ট, ভেন্যু এবং নির্দিষ্ট সিট বা আসনগুলো দেখানো।
  • ব্যবহারকারীদের নির্দিষ্ট সিট সিলেক্ট বা বাছাই করতে দেওয়া।
  • অর্থ প্রদানের সময় সাময়িকভাবে একটি সিট হোল্ড (hold) করা বা আটকে রাখা (যেমন, ১০ মিনিটের জন্য)।
  • সফলভাবে পেমেন্ট করার পর সিট বুক করা।
  • পেমেন্ট ফেইল হলে বা সময় পার হয়ে গেলে হোল্ড করা সিট আবার উন্মুক্ত করে দেওয়া।

নন-ফাংশনাল প্রয়োজনীয়তা (Non-Functional Requirements):

  • কোনোভাবেই ডাবল-বুকিং (double-booking) নয়: একই আসন কোনোভাবেই দুইজনকে বিক্রি করা যাবে না। এটি সবচেয়ে গুরুত্বপূর্ণ নিয়ম।
  • উচ্চ প্রাপ্যতা (High availability) সহ প্রচুর রিড (read): ইভেন্টের পেজটি অবশ্যই যেকোনো সময় লোড হতে হবে, কারণ বুকিংয়ের তুলনায় মানুষ বেশি পেজ ভিউ বা সার্চ করে (প্রায় ১০০:১ অনুপাত)।
  • বার্স্ট ট্রাফিক (Burst traffic): জনপ্রিয় কনসার্টের টিকিট ছাড়লে কয়েক মিনিটের মধ্যে লাখ লাখ মানুষ একইসাথে চেষ্টা করতে পারে।
  • ফেয়ারনেস বা সাম্যতা (Fairness): যারা আগে আসবেন, তারা আগে সিট পাবেন (First-come-first-served)।
সমস্যা: দুই জন ইউজার একইসাথে একই সিট বুক করতে চান!

অ্যাস্টিমেশন (Estimation)

চলুন এই সিস্টেমের সাইজ পরিমাপ করি:

  • প্রতি মাসে বুকিং: ১০ মিলিয়ন
  • হোল্ড (Holds) থেকে বুকিংয়ের অনুপাত: একটি সফল বুকিংয়ের বিপরীতে গড়ে ৩ বার সিট হোল্ড করা হয়।
  • রিড-রাইট (Read-to-Write) অনুপাত: ১০০:১। ১০০ জন মানুষ পেজ দেখলে মাত্র ১ জন বুকিং করেন।
  • গড় কিউপিএস (Average QPS): ~৪০০ রিড/সেকেন্ড, ~৪ রাইট/সেকেন্ড।
  • পিক কিউপিএস (Peak QPS): জনপ্রিয় ইভেন্টে এটি ১০০,০০০ রিড/সেকেন্ড এবং ১০,০০০ রাইট/সেকেন্ডে পৌঁছাতে পারে।

গড় ট্রাফিক খুব বেশি না হলেও, এখানকার মূল চ্যালেঞ্জ হলো এই পিক ট্রাফিক (peak traffic)। কারণ মাত্র কয়েক মিনিটের মধ্যেই ডেটাবেস থ্র্যাশিং (thrashing) বা ক্র্যাশ হতে পারে, তাই আমাদের এই ডেটাবেসটিকে খুব সাবধানতার সাথে রক্ষা করতে হবে।

কোর ডেটা মডেল (Core Data Model)

যেহেতু আমাদের ফাইন্যান্সিয়াল ডেটা বা লেনদেনের ডেটা নিয়ে কাজ করতে হবে এবং হার্ড কনসিস্টেন্সি (hard consistency) বা সব সময় সঠিক ডেটা প্রয়োজন (যাতে ডাবল বুকিং না হয়), তাই রিলেশনাল ডেটাবেস (RDBMS) যেমন MySQL বা PostgreSQL হলো এই কাজের জন্য সবচেয়ে সঠিক পছন্দ। এগুলোতে একটি চমৎকার ফিচার রয়েছে যা আমরা ব্যবহার করব — তা হলো ACID কমপ্লায়েন্স ট্রানজাকশন।

  • ইভেন্ট (Event): event_id, name, start_time, venue_id
  • ভেন্যু (Venue): venue_id, name, total_seats
  • সিট (Seat): seat_id, venue_id, row, number, status (Available, Held, Booked)
  • বুকিং (Booking): booking_id, user_id, event_id, seat_ids, status, created_at

নোট: Seat টেবিলটি প্রতিটি ইভেন্ট এবং ভেন্যুর জন্য আগে থেকেই তৈরি করা উচিত (প্রি-পপুলেটেড)। ডাইনামিকভাবে সিট বা আসন তৈরি করার চেয়ে, সেগুলোর স্ট্যাটাস আপডেট করা ডেটাবেসের জন্য অনেক বেশি সহজ।

ডেটা রিলেশন (Data Relations): ইভেন্ট, সিট এবং বুকিং
Click chart to zoom
বুকিং ফ্লো: একই সিট যাতে দুইবার বুক না হয়, সেজন্য 'SELECT FOR UPDATE' ব্যবহার করে প্যাসিমিস্টিক লকিং (pessimistic locking) বা ডেটাবেস লক করা হয়।

ডাবল বুকিং প্রতিরোধ (Preventing Double Booking)

যখন ইউজার A এবং ইউজার B একই মিলিসেকেন্ডে একই সিটে ক্লিক করেন তখন কী ঘটে? আমাদের নিশ্চিত করতে হবে যে মাত্র একজনই সফল হবেন। এখানে আমরা প্যাসিমিস্টিক লকিং (Pessimistic Locking) ব্যবহার করি।

আমরা ডেটাবেসে SELECT ... FOR UPDATE ব্যবহার করি। এটি ডেটাবেসকে নির্দেশ দেয় যে: "আমি এই সারিগুলো আপডেট করতে যাচ্ছি, তাই আমি শেষ না করা পর্যন্ত অন্য কাউকে এটি পড়তে বা লিখতে দিও না।"

  • ইউজার A-এর রিকোয়েস্ট: BEGIN TRANSACTION; SELECT * FROM Seats WHERE seat_id = 99 AND status = 'Available' FOR UPDATE;
  • ইউজার B-এর রিকোয়েস্ট: একই কোয়েরি চালায়। তবে ইউজার A-এর ট্রানজাকশন লক থাকায়, ডেটাবেস ইউজার B-কে অপেক্ষা করায়।
  • ইউজার A-এর রিকোয়েস্ট: সিটটি 'Held'-এ আপডেট করে ট্রানজাকশন কমিট (Commit) করে। এতে লকটি উন্মুক্ত হয়ে যায়।
  • ইউজার B-এর রিকোয়েস্ট: এখন লক ছাড়া পেয়ে দেখে status আর 'Available' নেই (এটি এখন 'Held')। তাই ইউজার B-কে ডেটাবেস ডিনাই (deny) করে দেয়।

এই লকগুলো অত্যন্ত দ্রুত এবং কেবল কয়েক মিলিসেকেন্ড স্থায়ী হয়, কারণ এটি কেবলমাত্র হোল্ড (hold) করার সময় লক করে, পুরো পেমেন্ট প্রক্রিয়ায় নয়। এটি ডাবল বুকিং বা দ্বিগুণ টিকিট হওয়া সম্পূর্ণরূপে রোধ করে।

বুকিং হোল্ড এবং টাইমআউট (Booking Holds and Timeouts)

যখন কোনো ব্যবহারকারী পেমেন্ট পেজে যান, তখন সিটগুলো সাময়িকভাবে আটকে বা হোল্ড (hold) করে রাখা হয় (যেমন, ১০ মিনিটের জন্য)। যদি তিনি পেমেন্ট না করেন বা উইন্ডোটি বন্ধ করে দেন, তবে আমাদের অবশ্যই সিটগুলোকে আবার Available করতে হবে।

এর জন্য সাধারণত একটি ডিলিড ব্যাকগ্রাউন্ড জব (Delayed background job) বা টাস্ক কিউ (Task Queue) (যেমন, RabbitMQ, SQS, অথবা Redis) ব্যবহার করা হয়:

  1. যখন সিটটি 'Held' করা হয়, তখন task: release_seat_if_unpaid, seat_id=99 মেসেজটি ১০ ​​মিনিটের বিলম্বের সাথে (delay) কিউতে (queue) পাঠানো হয়।
  2. ১০ মিনিট পর, একটি ওয়ার্কার (worker) কিউ থেকে মেসেজটি তুলে নেয় বা ডিকিউ (dequeue) করে।
  3. ওয়ার্কারটি ডেটাবেস পরীক্ষা করে देखता है — সিটটির স্ট্যাটাস কি এখনও 'Held'? (এর মানে পেমেন্ট প্রক্রিয়া সম্পূর্ণ হয়নি)।
  4. যদি হ্যাঁ হয়, তবে ওয়ার্কারটি স্ট্যাটাস আপডেট করে Available করে দেয়। যদি এটি ইতোমধ্যে 'Booked' হয়ে থাকে, তবে ওয়ার্কার কাজটিকে স্কিপ করে যায়।

ক্রোন জব (Cron jobs) (যেমন, প্রতি ১ মিনিটে ডেটাবেস স্ক্যান করা) ছোট স্কেলে কাজ করে, কিন্তু যখন কয়েক মিলিয়ন সারি বা ডেটা থাকে তখন এটি পুরো ডেটাবেসকে মারাত্মকভাবে ধীর করে দেয়। তাই ডিলিড টাস্কগুলো (delayed tasks) ব্যবহার করা অনেক বেশি স্কেলেবল।

স্কেলিং এবং ক্যাশিং (Scaling & Caching)

বার্স্ট ট্রাফিক সামলানো (Handling Burst Traffic)

যখন বিশ্বের সবচেয়ে জনপ্রিয় কোনো কনসার্টের টিকিট ছাড়ে, তখন সেকেন্ডে ১০০,০০০-এর বেশি মানুষ পেজে প্রবেশ করে, যা যেকোনো রিলেশনাল ডেটাবেসকে ক্র্যাশ করে দিতে সক্ষম।

১. রিড-এর জন্য ক্যাশিং (Caching for Reads):

  • ইভেন্টের বিবরণ এবং বর্তমানে উপলব্ধ সিটগুলো Redis ক্যাশ-এ রাখা হয়।
  • যখনই ডেটাবেসে কোনো সিটের স্ট্যাটাস পরিবর্তন হয়, ক্যাশ আপডেট করা হয়।
  • ব্যবহারকারীরা টিকিট দেখার সময় সরাসরি ক্যাশ থেকে সব দেখতে পান, এতে ডেটাবেসে কোনো প্রেসার বা লোড পড়ে না।

২. ভার্চুয়াল ওয়েটিং রুম (Virtual Waiting Rooms):

  • এটি মূলত একটি ফেয়ার, ফাস্ট-ইনফাস্ট-আউট (FIFO) কিউ বা সারি, যা ট্রাফিককে ধীরে ধীরে সিস্টেমে প্রবেশের অনুমতি দেয়।
  • সার্ভারগুলো ধারণক্ষমতার বেশি লোড হলে, নতুন ব্যবহারকারীদের ক্লাউডফ্লেয়ার (Cloudflare) অথবা লোড ব্যাল্যান্সার (Load Balancer) স্তরে একটি কিউতে বা লাইনে যোগ করে দেওয়া হয়।
  • এটি ট্রাফিক বার্স্টকে (burst) দীর্ঘ সময় ধরে বিন্যাস করে, যার ফলে ব্যাকএন্ড সার্ভারগুলোর ডেটাবেসকে একটি নিয়ন্ত্রিত মাত্রায় রিকোয়েস্ট পাঠানো সম্ভব হয়। এর ফলেই টিকিট সিস্টেম হঠাৎ করে লোড নিতে না পেরে বা বেশি মানুষের চাপে ডাউন হয় না।
ভার্চুয়াল ওয়েটিং রুমসহ সম্পূর্ণ আর্কিটেকচার
Note: ইন্টারভিউ টিপস: আপনি যদি রিলেশনাল ডেটাবেসের ACID প্রোপার্টি ব্যবহার করে ডাবল বুকিং প্রতিরোধের কারণ ব্যাখ্যা করতে না পারেন (যেমন SELECT FOR UPDATE), তবে এই ইন্টারভিউতে ফেইল করার সম্ভাবনা প্রবল। এছাড়াও ডিলিড টাস্কের (delayed tasks) ব্যাপারটা উল্লেখ করতে ভুলবেনলগ্ন না (যেটি পেমেন্ট না হলে সিট রিলিজ করতে ব্যবহৃত হয়) এবং ভার্চুয়াল ওয়েটিং রুমের বিষয়টিও মনে রাখবেন (যেটি ডেটাবেসের উপর অতিরিক্ত চাপ কমানোর জন্য ব্যবহার করা হয়)।

Key Metrics

ইভেন্ট পেজ লোড বা রিড (Read Event Page)
Redis ক্যাশ থেকে আনা হয়
< ৫০ ms \(O(1)\)
সিট বুক/হোল্ড করা (Hold Seat)
ডেটাবেসে প্যাসিমিস্টিক লকিং (Pessimistic DB lock)
~১০-৫০ ms \(O(\log N)\)
পিক ট্রাফিক সামলানো বা বার্স্ট ট্রাফিক (Burst capacity)
ভার্চুয়াল ওয়েটিং রুম ও ক্যাশ-এর মাধ্যমে
১০০,০০০+ কিউপিএস —
সিট রিলিজ (Seat release)
ডিলিড টাস্ক কিউয়ের মাধ্যমে
১০ মিনিট \(O(1)\)

ছোট কুইজ

টিকেট বুকিং সিস্টেমে ডাবল-বুকিং রোধ করার জন্য কোন পদ্ধতিটি সেরা?

পড়া চালিয়ে যান