ডিজাইন ডিপ ডাইভ (Design Deep Dive)
এখন আমরা হাই-লেভেল ডিজাইন নিয়ে আলোচনা করেছি, এবার নিচের বিষয়গুলো আরও গভীরভাবে দেখব:
- উন্নত ডেটা মডেল (Improved Data Model)
- কনকারেন্সি সমস্যা (Concurrency Issues)
- সিস্টেম স্কেলিং (Scaling the System)
- মাইক্রোসার্ভিস আর্কিটেকচারে ডেটা অসঙ্গতি সমাধান (Resolving Data Inconsistency in Microservice Architecture)
উন্নত ডেটা মডেল (Improved Data Model)
হাই-লেভেল ডিজাইনে যেমন বলা হয়েছে, যখন আমরা একটি হোটেল রুম রিজার্ভ করি, তখন আমরা আসলে নির্দিষ্ট কোনো রুম নয়, বরং একটি রুম টাইপ (Room Type) রিজার্ভ করি। এই নতুন প্রয়োজনীয়তা সমর্থন করতে API এবং ডাটাবেস স্কিমায় কী পরিবর্তন আনতে হবে?
রিজার্ভেশন API-তে roomID এর পরিবর্তে roomTypeID ব্যবহার করা হবে।
রিজার্ভেশন API:
POST /v1/reservationsRequest Parameters:
{
"startDate":"2021-04-28",
"endDate":"2021-04-30",
"hotelID":"245",
"roomTypeID":"12354673389",
"roomCount":"3",
"reservationID":"13422445"
}আপডেটেড স্কিমা Figure 6-এ দেখানো হয়েছে।
গুরুত্বপূর্ণ টেবিলসমূহ
room
রুম সংক্রান্ত তথ্য সংরক্ষণ করে।
room_type_rate
নির্দিষ্ট রুম টাইপের ভবিষ্যৎ তারিখভিত্তিক মূল্য সংরক্ষণ করে।
reservation
অতিথির রিজার্ভেশন সংক্রান্ত তথ্য সংরক্ষণ করে।
room_type_inventory
হোটেলের রুম ইনভেন্টরি সংরক্ষণ করে। এটি রিজার্ভেশন সিস্টেমের সবচেয়ে গুরুত্বপূর্ণ টেবিলগুলোর একটি।
কলামসমূহ:
hotel_id→ হোটেলের আইডিroom_type_id→ রুম টাইপের আইডিdate→ নির্দিষ্ট একটি তারিখtotal_inventory→ মোট উপলব্ধ রুম সংখ্যা (মেইনটেন্যান্সের জন্য সরিয়ে রাখা রুম বাদ দিয়ে)total_reserved→ নির্দিষ্ট হোটেল, রুম টাইপ এবং তারিখের জন্য বুক হওয়া রুম সংখ্যা
(hotel_id, room_type_id, date) একটি Composite Primary Key হিসেবে ব্যবহৃত হয়।
এই টেবিলের ডেটা ২ বছরের ভবিষ্যৎ তারিখ পর্যন্ত আগে থেকেই Pre-populate করা থাকে। প্রতিদিন একটি Scheduled Job চালিয়ে নতুন তারিখের জন্য ডেটা যুক্ত করা হয়।
স্টোরেজ হিসাব
ধরি:
- ৫,০০০টি হোটেল
- প্রতি হোটেলে ২০টি রুম টাইপ
- ২ বছরের ডেটা
তাহলে মোট Row সংখ্যা হবে:
5000 × 20 × 2 × 365
= 73,000,000 rows৭৩ মিলিয়ন Row খুব বড় নয় এবং একটি ডাটাবেসে সংরক্ষণ করা সম্ভব।
তবে High Availability নিশ্চিত করার জন্য একাধিক Region বা Availability Zone-এ Database Replication ব্যবহার করা যেতে পারে।
room_type_inventory এর নমুনা ডেটা
| hotel_id | room_type_id | date | total_inventory | total_reserved |
|---|---|---|---|---|
| 211 | 1001 | 2021-06-01 | 100 | 80 |
| 211 | 1001 | 2021-06-02 | 100 | 82 |
| 211 | 1001 | 2021-06-03 | 100 | 86 |
| 211 | 1001 | … | … | … |
| 211 | 1001 | 2023-05-31 | 100 | 0 |
| 211 | 1002 | 2021-06-01 | 200 | 16 |
| 2210 | 101 | 2021-06-01 | 30 | 23 |
| 2210 | 101 | 2021-06-02 | 30 | 25 |
রুম অ্যাভেইলেবিলিটি চেক
Input:
startDate = 2021-07-01
endDate = 2021-07-03
roomTypeId
hotelId
numberOfRoomsToReserveOutput:
True -> রুম পাওয়া যাবে
False -> রুম পাওয়া যাবে নাSQL Query:
SELECT date, total_inventory, total_reserved
FROM room_type_inventory
WHERE room_type_id = ${roomTypeId}
AND hotel_id = ${hotelId}
AND date BETWEEN ${startDate} AND ${endDate}রিটার্ন:
| date | total_inventory | total_reserved |
|---|---|---|
| 2021-07-01 | 100 | 97 |
| 2021-07-02 | 100 | 96 |
| 2021-07-03 | 100 | 95 |
অ্যাপ্লিকেশন যাচাই করবে:
if (
total_reserved + numberOfRoomsToReserve
<= total_inventory
)সব তারিখে শর্ত সত্য হলে রুম বুক করা যাবে।
10% Overbooking Support
if (
total_reserved + numberOfRoomsToReserve
<= 1.1 * total_inventory
)এর মাধ্যমে ১০% অতিরিক্ত বুকিং অনুমোদন করা সম্ভব।
যদি ডেটা খুব বড় হয়ে যায়?
সম্ভাব্য সমাধান:
1. পুরনো রিজার্ভেশন Archive করা
বর্তমান ও ভবিষ্যতের ডেটা মূল ডাটাবেসে রাখা হবে।
পুরনো ডেটা Archive বা Cold Storage-এ পাঠানো হবে।
2. Database Sharding
যেহেতু অধিকাংশ Query-তে hotel_id ব্যবহৃত হয়, তাই Sharding Key হিসেবে hotel_id ব্যবহার করা যায়।
hash(hotel_id) % number_of_serversকনকারেন্সি সমস্যা (Concurrency Issues)
Double Booking প্রতিরোধ করতে হবে।
দুই ধরনের সমস্যা হতে পারে:
- একই ব্যবহারকারী একাধিকবার Book বাটনে ক্লিক করে।
- একাধিক ব্যবহারকারী একই সময়ে একই রুম বুক করতে চায়।
Scenario 1: একই ব্যবহারকারীর একাধিক ক্লিক
ধরি ব্যবহারকারী দুইবার Book বাটনে ক্লিক করেছে।
ফলে একই রিজার্ভেশন দুইবার Insert হতে পারে।
সমাধান ১: Client-side Protection
Request পাঠানোর সাথে সাথে:
- Submit Button Disable করা
- Button Gray Out করা
- Button Hide করা
তবে JavaScript Disable থাকলে এটি কাজ নাও করতে পারে।
সমাধান ২: Idempotent API
Request-এর সাথে একটি Idempotency Key পাঠানো হয়।
এখানে:
reservation_idIdempotency Key হিসেবে ব্যবহার করা হয়েছে।
ধাপসমূহ
Step 1
রিজার্ভেশন অর্ডার তৈরি করা।
Step 2
সিস্টেম একটি Unique reservation_id জেনারেট করে।
Step 3a
প্রথম Request সফলভাবে Insert হয়।
Step 3b
দ্বিতীয় Request একই reservation_id ব্যবহার করায় Unique Constraint Violation হয়।
ফলে দ্বিতীয় Insert ব্যর্থ হয়।
Scenario 2: একাধিক ব্যবহারকারী একই রুম বুক করছে
ধরি:
total_inventory = 100
total_reserved = 99মাত্র ১টি রুম বাকি।
User 1 এবং User 2 একই সময়ে বুক করতে চায়।
দুজনেই:
99 + 1 <= 100দেখে True পায়।
ফলে দুজনেই বুক করে ফেলে।
এটাই Race Condition।
রুম রিজার্ভ করার SQL
Step 1
SELECT date,total_inventory,total_reserved
FROM room_type_inventory
WHERE room_type_id = ${roomTypeId}
AND hotel_id = ${hotelId}
AND date BETWEEN ${startDate} AND ${endDate}Validation
if (
total_reserved + numberOfRoomsToReserve
> 1.1 * total_inventory
){
rollback;
}Step 2
UPDATE room_type_inventory
SET total_reserved =
total_reserved + ${numberOfRoomsToReserve}
WHERE room_type_id = ${roomTypeId}
AND date BETWEEN ${startDate} AND ${endDate}COMMITOption 1: Pessimistic Locking
এটিকে Pessimistic Concurrency Control-ও বলা হয়।
একজন User Row Update শুরু করলে Row Lock হয়ে যায়।
অন্য User-দের অপেক্ষা করতে হয়।
MySQL উদাহরণ:
SELECT ... FOR UPDATEসুবিধা
- Concurrent Update প্রতিরোধ করে
- Implement করা সহজ
- Heavy Contention-এ কার্যকর
অসুবিধা
- Deadlock হতে পারে
- Scalability খারাপ
- দীর্ঘ Transaction Performance কমিয়ে দেয়
তাই Hotel Reservation System-এর জন্য এটি সাধারণত সুপারিশ করা হয় না।
Option 2: Optimistic Locking
Optimistic Concurrency Control নামে পরিচিত।
এখানে Database Lock ব্যবহার করা হয় না।
একটি অতিরিক্ত Column:
versionব্যবহার করা হয়।
প্রক্রিয়া
- Version Read
- Update
- Version + 1
- Validation Check
যদি Version Match না করে:
Transaction Abort
Retryসুবিধা
- Lock লাগে না
- Stale Data Update প্রতিরোধ করে
- Low Contention-এ দ্রুত
অসুবিধা
- High Contention-এ অনেক Retry হয়
- User Experience খারাপ হতে পারে
তবুও Hotel Reservation System-এর জন্য এটি ভালো সমাধান।
Option 3: Database Constraint
Constraint:
CONSTRAINT check_room_count
CHECK (
total_inventory - total_reserved >= 0
)যদি:
total_inventory = 100
total_reserved = 101হয়ে যায়, Constraint Violation হবে এবং Transaction Rollback হবে।
সুবিধা
- Implement করা সহজ
- Low Contention-এ ভালো কাজ করে
অসুবিধা
- High Contention-এ অনেক Failure হয়
- Constraint Version Control কঠিন
- সব Database Constraint Support করে না
Hotel Reservation System-এর জন্য এটিও একটি বাস্তবসম্মত সমাধান।
Scalability
সাধারণত Hotel Reservation System-এর Load খুব বেশি নয়।
কিন্তু Booking.com বা Expedia-এর মতো বড় সিস্টেমে QPS 1000 গুণ বেশি হতে পারে।
সব Service Stateless হওয়ায় Horizontal Scaling সহজ।
কিন্তু Database Stateful হওয়ায় সেটিই Bottleneck হয়।
Database Sharding
ডেটাকে একাধিক Database-এ ভাগ করা হয়।
Sharding Key:
hotel_idRouting:
hotel_id % 16যদি মোট QPS:
30,000হয়,
তাহলে ১৬টি Shard ব্যবহার করলে:
30,000 / 16
= 1,875 QPS প্রতি Shardযা একটি MySQL Server সহজেই Handle করতে পারে।
Caching
Hotel Inventory-এর একটি বিশেষ বৈশিষ্ট্য:
শুধুমাত্র বর্তমান ও ভবিষ্যতের Inventory গুরুত্বপূর্ণ।
Redis একটি ভালো পছন্দ কারণ:
- TTL Support করে
- LRU Eviction Support করে
Cache Layer
Cache Database-এর উপরে বসানো হয়।
অধিকাংশ Read Request Cache থেকে Serve হয়।
Inventory Cache Structure
Key:
hotelID_roomTypeID_date
Value:
Available Room CountInventory APIs
Query Inventory
নির্দিষ্ট Hotel, Room Type এবং Date Range-এর Available Room সংখ্যা ফেরত দেয়।
Reserve Room
total_reserved + 1Cancel Reservation
Inventory Update করে।
Cache Consistency Challenge
Booking-এর সময়:
Step 1
Inventory Cache থেকে Availability Check।
Step 2
Database Update।
Step 3
Asynchronously Cache Update।
CDC (Change Data Capture) ব্যবহার করে Database → Redis Sync করা যায়।
যেমন:
- Debezium
- Kafka Connect
Cache এবং Database অসঙ্গতি
Cache বলছে:
1 room availableDatabase বলছে:
0 room availableUser বুক করতে গেলে Database Final Validation করবে।
তাই Database-ই Source of Truth।
সুবিধা
- Database Load কমে
- Read Performance অনেক বেড়ে যায়
অসুবিধা
- Cache Consistency বজায় রাখা কঠিন
- User Experience প্রভাবিত হতে পারে
মাইক্রোসার্ভিসে ডেটা কনসিস্টেন্সি
Monolithic Architecture-এ:
একটি Shared Database
একটি Transaction
ACID GuaranteeMicroservice Architecture-এ:
প্রতিটি Service-এর নিজস্ব Databaseফলে:
একটি Logical Transaction
একাধিক Database জুড়ে ছড়িয়ে যায়এবং Data Consistency Challenge তৈরি হয়।
Two-Phase Commit (2PC)
Multiple Node-এ Atomic Commit নিশ্চিত করে।
সব সফল
অথবা
সব ব্যর্থসমস্যা
- Blocking Protocol
- Node Failure হলে পুরো Transaction আটকে যায়
- Performance কম
Saga Pattern
Saga হলো Local Transaction-এর একটি ধারাবাহিকতা।
প্রতিটি Transaction:
- Update করে
- Event Publish করে
- পরবর্তী Service Trigger করে
কোনো Step ব্যর্থ হলে:
Compensating Transactionচালিয়ে পূর্ববর্তী পরিবর্তন Undo করা হয়।
পার্থক্য
2PC:
Single ACID TransactionSaga:
Multiple Transactions
Eventual ConsistencyMicroservice-এ Data Consistency বজায় রাখা অত্যন্ত জটিল।
এই ডিজাইনে Pragmatic Approach হিসেবে Reservation Data এবং Inventory Data একই Relational Database-এ রাখা হয়েছে, যাতে ACID Property ব্যবহার করে Concurrency এবং Consistency সমস্যা সহজে সমাধান করা যায়।