السيطرة على تضارب المخزون في المتاجر: الأقفال، ريديس، والاستعلامات الذرية
حلول جذرية لمشاكل الشراء المتزامن ونفاد المخزون: القفل التفاؤلي، القفل التشاؤمي، والأقفال الموزعة عبر ريديس.
📦 Race Condition & Inventory Concurrency
<img src="https://img.shields.io/badge/System%20Design-Concurrency-blue" alt="Concurrency"/> <img src="https://img.shields.io/badge/Database-Transactions-green" alt="Transactions"/> <img src="https://img.shields.io/badge/Reliability-Inventory-orange" alt="Inventory"/> <img src="https://img.shields.io/badge/Interview-System%20Design-purple" alt="System Design Interview"/>سؤال انترفيو بسيط ظاهريًا... لكنه يكشف هل أنت فاهم الـConcurrency والـDatabase فعلاً.
</div>عندك منتج باقي منه قطعة واحدة فقط، واتنين Users ضغطوا Buy Now في نفس اللحظة. إيه اللي هيحصل؟
📚 Table of Contents
- السؤال
- المشكلة
- Race Condition
- ليه if quantity > 0 مش كفاية؟
- هل Transaction تحل المشكلة؟
- Isolation Levels
- Pessimistic Locking
- Optimistic Concurrency
- Atomic Update
- Distributed Locks
- هل Redis Lock هو الحل دائمًا؟
- Payment vs Inventory
- Inventory Reservation
- Reservation Expiration
- Saga Pattern
- Eventual Consistency
- Idempotency
- State Machine
- Database Constraints
- مثال SQL
- Architecture
- Production Scenarios
- Interview Answer
- Common Wrong Answers
- Production Checklist
- Official Sources
🎤 السؤال
الـinterviewer يقول:
عندك منتج باقي منه قطعة واحدة فقط. واتنين Users ضغطوا Buy Now في نفس الثانية. إيه اللي هيحصل؟
لو فكرت بشكل بسيط:
Request 1 arrives
↓
Take product
↓
Request 2 fails
الموضوع مش مضمون بالشكل ده.
لأن:
Arrival order مش هو نفسه execution order.
والـrequests ممكن تتنفذ concurrently.
📦 الحالة الأولية
عندنا:
Product ID: 101
Quantity: 1
وفي نفس الوقت:
User A → Buy Now
User B → Buy Now
💥 المشكلة
Request A:
SELECT quantity
FROM products
WHERE id = 101;
Database:
quantity = 1
قبل ما A يعمل update...
Request B يوصل:
SELECT quantity
FROM products
WHERE id = 101;
Database:
quantity = 1
دلوقتي:
A sees 1
B sees 1
الاتنين مقتنعين إن المنتج متاح.
ثم:
A → UPDATE quantity = 0
B → UPDATE quantity = 0
والنتيجة:
Database:
quantity = 0
Orders:
Order A = Paid
Order B = Paid
يعني:
بعتنا قطعتين وإحنا أصلًا عندنا قطعة واحدة.
🧨 Race Condition
المشكلة دي اسمها:
Race Condition
يعني نتيجة العملية تعتمد على توقيت وترتيب تنفيذ عمليات concurrent.
وفي سياق قواعد البيانات ممكن نتكلم عن:
Concurrent Update
Lost Update
Write Conflict
Overselling
المصطلح الدقيق يعتمد على الـimplementation والـdatabase behavior.
❌ ليه if(quantity > 0) مش كفاية؟
ناس كتير تعمل:
if (product.quantity > 0) {
product.quantity -= 1;
await product.save();
}
شكلها منطقي.
لكن المشكلة:
READ
↓
CHECK
↓
WRITE
الـcheck والـwrite مش بالضرورة atomic.
ممكن يحصل:
A: READ quantity = 1
B: READ quantity = 1
A: CHECK > 0 ✓
B: CHECK > 0 ✓
A: WRITE 0
B: WRITE 0
فالـcheck نفسها مش المشكلة.
المشكلة إن:
القرار والـupdate مش مربوطين بعملية concurrency-safe واحدة.
🔐 هل Transaction تحل المشكلة؟
هنا لازم تكون حذر.
ناس كتير تقول:
"هحطها في Transaction وخلاص."
لكن:
BEGIN TRANSACTION
SELECT quantity
UPDATE quantity
COMMIT
مش معناه تلقائيًا إنك اخترت concurrency behavior المناسب.
الـdatabase عندها:
Isolation Level
+
Locking
+
MVCC / Row Versioning
+
Constraints
والسلوك يختلف حسب الـdatabase والـisolation configuration.
Microsoft توضح أن transaction isolation level يحدد كيفية حماية القراءات من تعديلات transactions الأخرى، وأن اختيار مستوى أعلى يقلل بعض concurrency anomalies لكنه قد يزيد blocking والـresource usage. citeturn0search1turn0search2
يعني:
Transaction ≠ magic concurrency button.
🧱 Isolation Levels
بشكل مبسط، أشهر المستويات:
| Isolation Level | الفكرة |
|---|---|
READ UNCOMMITTED | أقل حماية، وقد يسمح بقراءات غير ملتزمة |
READ COMMITTED | يمنع dirty reads |
REPEATABLE READ | يحافظ على ثبات الصفوف المقروءة داخل transaction |
SERIALIZABLE | أقوى عزل تقليديًا ويمنع مجموعة أكبر من التداخلات |
SNAPSHOT | يعتمد على row versions في الأنظمة التي تدعمه |
لكن خلي بالك:
الأسماء واحدة تقريبًا، لكن التفاصيل والimplementation تختلف بين PostgreSQL وSQL Server وMySQL وغيرها.
SQL Server مثلًا يدعم locking وrow versioning، ويوضح أن SERIALIZABLE يوفر أعلى مستوى عزل تقليديًا لكنه قد يزيد blocking في الأنظمة متعددة المستخدمين. citeturn0search1
🔒 Pessimistic Locking
الفكرة:
أنا متوقع conflict، فهقفل الـrow أثناء العملية.
مثلاً conceptually:
BEGIN
SELECT *
FROM products
WHERE id = 101
FOR UPDATE;
if quantity > 0:
UPDATE products
SET quantity = quantity - 1;
COMMIT
في PostgreSQL، SELECT ... FOR UPDATE هو locking clause يؤثر على طريقة قفل الصفوف التي يتم الحصول عليها من الجدول، ويمكن استخدامه لمنع عمليات متعارضة من تعديل الصف قبل انتهاء transaction. citeturn0search8turn0search4
السيناريو:
Request A
↓
Lock row
↓
quantity = 1
↓
decrement
↓
commit
↓
unlock
Request B
↓
wait
↓
read quantity = 0
↓
reject
الميزة
قوي جدًا عندما يكون عندك:
High contention
+
Short transaction
+
Critical shared resource
العيب
لو عندك traffic ضخم على نفس المنتج:
Thousands of users
↓
One hot row
↓
Lock contention
↓
Waiting
فالـthroughput ممكن يتأثر.
⚡ Optimistic Concurrency
بدل ما تقفل الصف:
افترض إن الـconflicts قليلة، واكتشفها وقت الـupdate.
نضيف:
version
مثلاً:
id = 101
quantity = 1
version = 7
A يقرأ:
quantity = 1
version = 7
B يقرأ:
quantity = 1
version = 7
A ينفذ:
UPDATE products
SET quantity = 0,
version = 8
WHERE id = 101
AND version = 7
AND quantity > 0;
A ينجح.
B يحاول:
UPDATE products
SET quantity = 0,
version = 8
WHERE id = 101
AND version = 7
AND quantity > 0;
لكن:
version = 8
فـ:
Rows affected = 0
B يعرف إن حد سبقه.
Microsoft توثق optimistic concurrency كآلية تتحقق وقت الـupdate مما إذا كان الصف قد تغير بعد أن تمت قراءته؛ وإذا حصل conflict يفشل التحديث ويحتاج التطبيق للتعامل معه. citeturn0search5turn0search1
🚀 Atomic Update
في سيناريو inventory البسيط، فيه حل مهم جدًا أحيانًا يكون أبسط من:
SELECT
↓
if
↓
UPDATE
وهو إنك تخلي قاعدة البيانات تنفذ الشرط والخصم في statement واحدة:
UPDATE products
SET quantity = quantity - 1
WHERE id = 101
AND quantity > 0;
وبعدين تشوف:
rows affected
لو:
rows affected = 1
يبقى حجزت قطعة.
لو:
rows affected = 0
يبقى مفيش stock متاح.
الفكرة المهمة:
خلي الـdatabase نفسها تنفذ الـcondition + mutation كعملية واحدة عندما يكون ذلك مناسبًا.
وده غالبًا حل ممتاز لعملية inventory decrement بسيطة.
🧠 ليه Atomic Update قوي؟
بدل:
Read
↓
Application decides
↓
Write
تخلي:
Database decides + writes
في statement واحدة:
UPDATE ... WHERE quantity > 0
وبالتالي تقلل window اللي ممكن يحصل فيها race.
لكن لو business logic معقد جدًا مثل:
reserve inventory
+
create order
+
apply promotion
+
allocate warehouse
+
publish event
ساعتها تحتاج تصميم أكبر.
🌍 Distributed Lock
طيب لو عندك:
App Server 1
App Server 2
App Server 3
App Server 4
ممكن حد يقول:
"نستخدم lock على السيرفر."
لكن ده مش كفاية.
لأن:
Server 1 → lock
Server 2 → doesn't know
Server 3 → doesn't know
فممكن تحتاج distributed coordination.
أحد الخيارات:
Redis
مع distributed locking pattern.
Redis توثق distributed locks كـprimitive للتنسيق بين processes التي تحتاج mutual exclusion، وتعرض Redlock كخوارزمية distributed lock، لكنها نفسها تحذر من افتراض أن lock يظل مضمونًا طوال حياة العملية وتناقش الحاجة إلى safeguards مثل fencing tokens في بعض السيناريوهات. citeturn0search0
⚠️ هل Redis Lock هو الحل دائمًا؟
لا.
دي نقطة interview مهمة جدًا.
مش كل مشكلة concurrency محتاجة:
Redis Lock
لو قاعدة البيانات هي أصل الحقيقة:
Inventory
فغالبًا الأفضل تبدأ بحل database-native:
Atomic Update
+
Transaction
+
Concurrency Control
+
Constraints
قبل ما تضيف distributed lock.
ليه؟
لأن Redis lock يضيف:
Network dependency
Lock expiration
Failure modes
Clock / timing concerns
Operational complexity
وممكن تفضل محتاج database concurrency protection أصلًا.
يعني:
متستخدمش distributed lock فقط لأنك عندك أكثر من server.
💳 طيب لو الدفع حصل قبل حجز المخزون؟
هنا الموضوع بقى أخطر.
السيناريو:
User A
↓
Payment succeeds
User B
↓
Payment succeeds
Inventory
↓
1 item
وبعدين:
A gets item
B has no item
ماذا تفعل؟
Refund B?
Cancel order?
Alternative product?
Waitlist?
وهنا نبدأ ندخل في:
Inventory Reservation
Saga
Compensation
Eventual Consistency
📦 Inventory Reservation
بدل ما تقول:
Pay first
↓
Check inventory
ممكن تعمل:
Create Order
↓
Reserve Inventory
↓
Start Payment
↓
Payment Success
↓
Confirm Reservation
مثلاً:
Available = 10
Reserved = 0
Reserve 1
Available = 9
Reserved = 1
ولو الدفع فشل:
Release reservation
Available = 10
Reserved = 0
⏳ Reservation Expiration
المشكلة:
المستخدم يحجز المنتج:
Reserved
وبعدين يختفي.
لو reservation فضلت للأبد:
Available stock
↓
0
رغم إن مفيش حد دفع.
عشان كده ممكن تعمل:
reservation_expires_at
مثلاً:
Reservation
TTL = 10 minutes
لو payment ما اكتملش:
Expired
↓
Release stock
🔄 Saga Pattern
في distributed system:
Inventory Service
Payment Service
Order Service
Notification Service
مفيش transaction واحدة سهلة تغطي كل الخدمات.
ممكن تعمل workflow:
Reserve Inventory
↓
Create Payment
↓
Payment Success
↓
Confirm Inventory
↓
Order Confirmed
ولو payment فشل:
Payment Failed
↓
Release Inventory
↓
Order Cancelled
دي فكرة من:
Saga / Compensating Actions
المهم تفهم إن compensation مش "rollback سحري".
أنت بتعمل عملية عكسية business operation.
مثلاً:
Reserve
تعويضها:
Release
مش:
ROLLBACK
على كل الخدمات.
🌊 Eventual Consistency
في distributed systems، ممكن لفترة قصيرة الحالات تكون:
Order = Pending
Payment = Processing
Inventory = Reserved
مش لازم كل services تتحدث في نفس اللحظة.
وبعد شوية:
Order = Confirmed
Payment = Paid
Inventory = Reserved
وده:
Eventual Consistency
يعني النظام يصل لحالة متسقة في النهاية، بدل ما تفرض transaction distributed ضخمة على كل service.
🔁 Idempotency
تخيل:
Reserve Inventory
request اتبعت مرتين بسبب retry.
مينفعش:
Reserve 1
Reserve 1
فتخسر قطعتين.
لازم يكون عندك identifier للعملية:
reservationId
idempotencyKey
orderId
وتضمن إن:
same logical request
=
same operation
مثلاً:
reserve(orderId=123, productId=101)
لو نفس الطلب وصل مرة ثانية:
return existing reservation
بدل ما تعمل reservation جديدة.
🧱 Database Constraints
ممكن تعمل safeguards إضافية.
مثلاً في بعض designs، تقدر تضمن إن:
quantity >= 0
عن طريق database constraint.
لكن خلي بالك:
الـconstraint يمنع الحالة غير الصالحة، لكنه مش بديل عن تصميم concurrency كامل.
هو safety net، مش كل الحل.
🗃️ Inventory State
بدل:
quantity
فقط، الأنظمة الأكبر ممكن تحتاج:
available
reserved
sold
مثلاً:
Inventory
available = 5
reserved = 2
sold = 10
ويكون عندك transitions واضحة:
Available
↓
Reserved
↓
Sold
أو:
Reserved
↓
Expired
↓
Available
🧠 State Machine
دي مهمة جدًا.
بدل ما أي endpoint يعمل:
order.status = "paid"
فكر في transitions:
PENDING
↓
RESERVED
↓
PAYMENT_PENDING
↓
PAID
↓
CONFIRMED
وممكن:
PAYMENT_PENDING
↓
PAYMENT_FAILED
↓
CANCELLED
أو:
RESERVED
↓
EXPIRED
↓
AVAILABLE
ده يمنع transitions غير منطقية.
مثلاً:
CANCELLED
↓
PAID
مينفعش.
🧮 مثال SQL بسيط
❌ الشكل الخطر
SELECT quantity
FROM products
WHERE id = 101;
-- application checks quantity > 0
UPDATE products
SET quantity = quantity - 1
WHERE id = 101;
فيه race window بين:
SELECT
و:
UPDATE
✅ Atomic Update
UPDATE products
SET quantity = quantity - 1
WHERE id = 101
AND quantity > 0;
ثم:
rows affected = 1
يعني:
Reservation succeeded
و:
rows affected = 0
يعني:
Out of stock
🔐 Optimistic Version Example
UPDATE products
SET
quantity = quantity - 1,
version = version + 1
WHERE
id = 101
AND quantity > 0
AND version = 7;
لو:
rows affected = 1
نجح.
لو:
rows affected = 0
حصل conflict أو stock غير متاح.
🏗️ Architecture
flowchart LR
U1["User A"]
U2["User B"]
API["Order API"]
INV["Inventory Service"]
DB[("Inventory DB")]
PAY["Payment Service"]
PG["Payment Provider"]
Q[["Queue / Events"]]
ORD["Order Service"]
OBS["Monitoring"]
U1 --> API
U2 --> API
API --> INV
INV --> DB
INV -->|"Reserve"| Q
API --> ORD
API --> PAY
PAY --> PG
PG -->|"Payment Event"| PAY
PAY --> ORD
ORD --> Q
Q --> INV
Q --> OBS
API --> OBS
INV --> OBS
PAY --> OBS
🔥 Production Flow
في نظام production، ممكن يكون flow مثل:
User
↓
Buy Now
↓
Create Order
↓
Idempotency Check
↓
Reserve Inventory Atomically
↓
Create Payment
↓
Customer Authentication
↓
Payment Result
↓
Confirm Order
↓
Commit Reservation
ولو payment failed:
Payment Failed
↓
Release Reservation
↓
Cancel Order
ولو reservation failed:
Out of Stock
↓
Don't start payment
وده مهم جدًا:
متاخدش فلوس العميل قبل ما تعرف إن عندك طريقة آمنة لتنفيذ الطلب.
لكن الـexact ordering يعتمد على business requirements وpayment provider وrisk model.
🚨 Production Scenarios لازم تفكر فيها
Scenario 1 — Double Click
Buy
Buy
الحل:
Idempotency
Scenario 2 — Two Users / One Item
A → Reserve
B → Reserve
الحل:
Atomic update
or
Pessimistic locking
or
Optimistic concurrency
حسب السيناريو.
Scenario 3 — Payment succeeds, inventory fails
Payment = Paid
Inventory = Failed
الحل:
Compensation
+
Refund
or
Alternative fulfillment
Scenario 4 — Inventory reserved, payment fails
Reserved
↓
Payment Failed
الحل:
Release reservation
Scenario 5 — Worker crashes
Reserve
↓
Worker dies
لازم يكون عندك:
Timeout
Recovery
Reconciliation
Scenario 6 — Duplicate event
PaymentSucceeded
PaymentSucceeded
لازم:
Idempotent event processing
Scenario 7 — Reservation expires while payment is processing
دي أصعب.
ممكن:
Reservation TTL expires
↓
Stock released
↓
Payment succeeds
لازم يكون عندك business rule واضح يمنع fulfillment غير الصحيح.
❌ Common Wrong Answers
❌ "أول request يوصل ياخد المنتج"
مش كفاية.
لأن الـrequests ممكن تتداخل أثناء التنفيذ.
❌ "هعمل if(quantity > 0)"
مش كفاية.
الـcheck والـupdate لازم يكون بينهم concurrency-safe mechanism.
❌ "هستخدم Transaction"
إجابة ناقصة.
اسأل:
Which isolation level?
Which locks?
Which database?
What happens on conflict?
❌ "هستخدم Redis Lock وخلاص"
غلط كـdefault answer.
ابدأ بالـdatabase correctness إذا كانت قاعدة البيانات هي source of truth، وبعدها أضف distributed coordination عند الحاجة.
❌ "هخصم بعد الدفع"
ممكن يؤدي إلى:
Paid
+
Out of Stock
لو stock لم يتم حجزه مسبقًا.
❌ "هحجز المنتج للأبد"
غلط.
لازم:
Expiration
+
Release
❌ "هستخدم Saga وهيحل كل حاجة"
Saga مش magic rollback.
هي:
Forward actions
+
Compensating actions
🎯 Interview Answer
لو الـinterviewer سألك:
"عندك منتج باقي منه قطعة واحدة واتنين users ضغطوا Buy Now في نفس اللحظة، تعمل إيه؟"
ممكن تجاوب:
"دي race condition في الـinventory. أول حاجة مش هعتمد على
if(quantity > 0)في application code لأن الـread والـupdate ممكن يحصل بينهم interleaving. حسب الـdatabase والـcontention، ممكن أستخدم atomic conditional update مثلUPDATE ... SET quantity = quantity - 1 WHERE id = ? AND quantity > 0، وأتحقق من rows affected. لو الـbusiness logic أعقد ممكن أستخدم transaction مع pessimistic locking أو optimistic concurrency باستخدام version column. ولو عندي distributed services، هفكر في reservation model بدل ما أربط الدفع بالمخزون مباشرة. أعمل inventory reservation بمدة محددة، وبعدها أبدأ payment، ولو payment نجح أconfirm reservation، ولو فشل أو انتهت المهلة أrelease الـinventory. كمان كل العملية لازم تكون idempotent عشان retries وdouble clicks، وعندي state machine وreconciliation للتعامل مع الحالات اللي تقع في النص."
ولو سألك:
"طيب لو payment نجح والمخزون فشل؟"
جاوب:
"دي distributed transaction problem. مش هقدر أعمل database rollback للـpayment provider. محتاج compensation، مثل refund، أو fulfillment بديل حسب الـbusiness rules. وده أحد الأسباب اللي تخلي inventory reservation قبل الدفع مفيد في بعض الأنظمة."
🧠 أهم Mental Model
احفظها كده:
Concurrency
↓
Race Condition
↓
Atomicity / Locking / Optimistic Concurrency
↓
Inventory Reservation
↓
Payment
↓
Compensation
↓
Idempotency
↓
State Machine
↓
Reconciliation
وأهم سؤال:
"ماذا يحدث لو عمليتان وصلوا في نفس الوقت؟"
وبعده:
"ماذا يحدث لو العملية الأولى نجحت في خطوة والثانية فشلت؟"
وبعده:
"ماذا يحدث لو حصل crash بين الخطوتين؟"
لو بدأت تفكر بالثلاثة دول، أنت خرجت من مستوى:
CRUD Developer
وبدأت تدخل:
System Design
+
Concurrency
+
Distributed Systems
🏆 Production Checklist
Inventory
- Atomic stock decrement
- Correct concurrency strategy
- Quantity cannot become negative
- Reservation model if needed
- Reservation expiration
- Release mechanism
- Inventory state machine
Database
- Correct isolation level
- Short transactions
- Proper indexes
- Optimistic versioning where useful
- Pessimistic locks where useful
- Constraints as safety nets
Orders
- Idempotency
- Unique order/payment attempt
- Valid state transitions
- Retry-safe operations
Payment
- Don't charge blindly before inventory decision
- Payment state machine
- Webhook verification
- Refund/compensation path
- Reconciliation
Distributed Systems
- Don't assume atomicity across services
- Saga/compensation where appropriate
- Eventual consistency understood
- Queue retry strategy
- Duplicate event handling
- Timeouts
- Monitoring
📚 Official Sources
Microsoft SQL Server
- Microsoft — Transaction Locking and Row Versioning Guide
- Microsoft — Transaction Isolation Levels
- Microsoft — Optimistic Concurrency
Microsoft's documentation explains both pessimistic and optimistic concurrency, row/version-based mechanisms, transaction isolation, and the trade-off between stronger isolation and concurrency. citeturn0search1turn0search5
PostgreSQL
PostgreSQL documents FOR UPDATE, NOWAIT, and SKIP LOCKED, as well as its transaction isolation behavior. citeturn0search4turn0search8
Redis
Redis documents distributed locking and explicitly discusses failure/consistency considerations around lock expiration and fencing tokens. citeturn0search0
<div align="center">
📦 One item. Two buyers. One database.
The hard part isn't quantity--.
The hard part is making the entire system correct when everything happens at the same time.
Concurrency is where CRUD ends and System Design begins.
</div>منشورات مقترحة
مشاريع ذات صلة
متجر إلكتروني متكامل يتيح تصفح المنتجات وإدارة السلة عبر Redux ومعالجة الدفع الآمن عبر Stripe مع حماية تكرار الطلبات.