🎯 মনে রাখার মন্ত্র:
“ছোট ID আগে, বড় ID পরে – Deadlock থাকবে না আর!”
Django Deadlock সমস্যা ও সমাধান: দারাজের উদাহরণ দিয়ে বাস্তব শিক্ষা
ভূমিকা: একটা পিজ্জা-কোকের গল্প
মনে করুন, দুইটা বাচ্চা আছে – রহিম আর করিম। টেবিলে আছে একটা পিজ্জা 🍕 আর একটা কোক 🥤।
নিয়ম হলো: দুইটা জিনিস একসাথে না নিলে খাওয়া যাবে না।
এখন দেখুন কী হলো:
- রহিম পিজ্জা ধরে রাখলো, কোকের জন্য অপেক্ষা করছে
- করিম কোক ধরে রাখলো, পিজ্জার জন্য অপেক্ষা করছে
দুইজনেই আটকে গেল! কেউ কাউকে ছাড়ছে না, কেউ খেতে পারছে না। এটাই Deadlock! 🔒
এই একই সমস্যা আমাদের Django অ্যাপ্লিকেশনেও হতে পারে। আজকে আমরা দেখবো কীভাবে এই সমস্যা এড়ানো যায়।
দারাজ Flash Sale: Real World Scenario
গত সপ্তাহে আমাদের একটা ই-কমার্স সাইটে ঠিক এই সমস্যা হয়েছিল। Flash Sale চলছিল, হাজার হাজার মানুষ একসাথে order করছিল।
Stock ছিল:
- iPhone 15: মাত্র 1টা
- Samsung S24: 2টা
- Wireless Mouse: 1টা
রহিম আর করিম দুইজনেই একসাথে order করলো। ফলাফল? সিস্টেম হ্যাং হয়ে গেল! 😱
Django Models – একদম সহজ
আসুন প্রথমে আমাদের Django models দেখি:
Product Model:
from django.db import models
from django.contrib.auth.models import User
from decimal import Decimal
class Product(models.Model):
name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.IntegerField(default=0)
sku = models.CharField(max_length=50, unique=True)
class Meta:
ordering = ['id']
def __str__(self):
return f"{self.name} - Stock: {self.stock}"
SalesOrder Model:
class SalesOrder(models.Model):
STATUS_CHOICES = [
('pending', 'অপেক্ষমান'),
('confirmed', 'নিশ্চিত'),
('cancelled', 'বাতিল'),
]
customer = models.ForeignKey(User, on_delete=models.CASCADE)
total_amount = models.DecimalField(max_digits=10, decimal_places=2)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
created_at = models.DateTimeField(auto_now_add=True)
OrderItem Model:
class OrderItem(models.Model):
order = models.ForeignKey(SalesOrder, on_delete=models.CASCADE, related_name='items')
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.IntegerField(default=1)
price = models.DecimalField(max_digits=10, decimal_places=2)
CartItem Model:
class CartItem(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.IntegerField(default=1)
গল্প ১: Single Product – iPhone এর যুদ্ধ
পরিস্থিতি: দারাজে iPhone 15 এর stock মাত্র 1টা। রহিম আর করিম দুইজনেই একসাথে “Buy Now” বাটনে ক্লিক করলো!
❌ সমস্যা: Race Condition
খারাপ Code:
from django.views import View
from django.http import JsonResponse
class BadOrderView(View):
def post(self, request):
user = request.user
product_id = request.POST.get('product_id')
quantity = int(request.POST.get('quantity', 1))
# Step 1: Product নাও
product = Product.objects.get(id=product_id)
# Step 2: Stock check করো
if product.stock >= quantity:
# ⚠️ সমস্যা: এই মুহূর্তে আরেকজন কিনে নিতে পারে!
time.sleep(0.1) # কিছু সময় লাগলো
# Step 3: Stock কমাও
product.stock -= quantity
product.save()
return JsonResponse({'success': True})
else:
return JsonResponse({'error': 'Stock নাই!'})
কী হবে?
Timeline:
0.0s - রহিম: Product নিলো (stock = 1)
0.0s - করিম: Product নিলো (stock = 1)
0.1s - রহিম: stock >= 1? হ্যাঁ! ✓
0.1s - করিম: stock >= 1? হ্যাঁ! ✓
0.2s - রহিম: stock = 1 - 1 = 0
0.2s - করিম: stock = 1 - 1 = 0
Result: দুইজনেই কিনে ফেললো! Stock = -1 😱
✅ সমাধান ১: select_for_update() – Database Lock
ভালো Code:
from django.db import transaction
class SafeOrderView(View):
def post(self, request):
user = request.user
product_id = request.POST.get('product_id')
quantity = int(request.POST.get('quantity', 1))
try:
with transaction.atomic():
# 🔒 Product lock করো!
product = Product.objects.select_for_update().get(id=product_id)
# Stock check
if product.stock >= quantity:
# Stock কমাও
product.stock -= quantity
product.save()
# Order তৈরি করো
order = SalesOrder.objects.create(
customer=user,
total_amount=product.price * quantity,
status='confirmed'
)
OrderItem.objects.create(
order=order,
product=product,
quantity=quantity,
price=product.price
)
return JsonResponse({
'success': True,
'message': f'{product.name} successfully ordered!',
'remaining_stock': product.stock
})
else:
return JsonResponse({
'error': f'Stock নাই! মাত্র {product.stock}টা আছে'
}, status=400)
except Exception as e:
return JsonResponse({'error': str(e)}, status=400)
কীভাবে কাজ করে?
0.0s - রহিম: Product lock 🔒 করলো
0.0s - করিম: Product lock চাইছে... অপেক্ষা করছে ⏳
0.2s - রহিম: Stock check + update complete
0.2s - রহিম: Lock release 🔓
0.2s - করিম: এখন lock 🔒 পেল
0.3s - করিম: Stock check... 0টা আছে!
0.3s - করিম: Error: "Stock নাই!"
Result: শুধু রহিম পেল! ✅ সঠিক!
✅ সমাধান ২: F() Expression – No Lock Needed!
এটা সবচেয়ে smart approach! Database এ directly calculation হয়, lock লাগে না!
Smart Code:
from django.db.models import F
class SmartOrderView(View):
def post(self, request):
user = request.user
product_id = request.POST.get('product_id')
quantity = int(request.POST.get('quantity', 1))
try:
with transaction.atomic():
# 🚀 Magic: Database এই check + update একসাথে!
updated = Product.objects.filter(
id=product_id,
stock__gte=quantity # যদি stock >= quantity হয়
).update(
stock=F('stock') - quantity # তাহলে কমাও
)
if updated:
# Updated হয়েছে মানে stock ছিল
product = Product.objects.get(id=product_id)
# Order তৈরি করো
order = SalesOrder.objects.create(
customer=user,
total_amount=product.price * quantity,
status='confirmed'
)
OrderItem.objects.create(
order=order,
product=product,
quantity=quantity,
price=product.price
)
return JsonResponse({
'success': True,
'message': f'{product.name} কিনেছেন!'
})
else:
# Stock ছিল না!
product = Product.objects.get(id=product_id)
return JsonResponse({
'error': f'Stock শেষ! মাত্র {product.stock}টা আছে'
}, status=400)
except Exception as e:
return JsonResponse({'error': str(e)}, status=400)
সুবিধা:
- Lock লাগে না = দ্রুত!
- Database atomic operation = নিরাপদ!
- High traffic এর জন্য perfect!
গল্প ২: Multiple Products – আরো বড় সমস্যা!
এখন পরিস্থিতি আরো জটিল!
দারাজ Cart:
রহিমের Cart:
- Samsung Phone (Product ID: 5) – 1টা
- Wireless Mouse (Product ID: 2) – 1টা
করিমের Cart:
- Wireless Mouse (Product ID: 2) – 1টা
- Samsung Phone (Product ID: 5) – 1টা
দুইজনেই একসাথে “Place Order” করলো!
❌ Deadlock এর বিপদ!
খারাপ Code:
def bad_multi_checkout(user_id):
cart_items = CartItem.objects.filter(user_id=user_id)
with transaction.atomic():
for item in cart_items:
# যে order এ cart এ আছে, সেই order এ lock!
product = Product.objects.select_for_update().get(id=item.product_id)
if product.stock >= item.quantity:
product.stock -= item.quantity
product.save()
কী হবে?
Visual Timeline:
Thread 1 (রহিম):
→ Samsung (ID: 5) lock 🔒 করলো
→ Mouse (ID: 2) lock চাইছে...
→ কিন্তু Mouse করিম নিয়ে গেছে! ⏳ অপেক্ষা...
Thread 2 (করিম):
→ Mouse (ID: 2) lock 🔒 করলো
→ Samsung (ID: 5) lock চাইছে...
→ কিন্তু Samsung রহিম নিয়ে গেছে! ⏳ অপেক্ষা...
Result: DEADLOCK! 💀
দুইজনেই চিরকাল অপেক্ষা করবে!
✅ সমাধান ৩: Sorting Magic – Deadlock Free!
সবচেয়ে সহজ ও কার্যকর সমাধান!
Key Idea: সবসময় ছোট ID থেকে বড় ID তে lock করো!
Perfect Code:
from django.views import View
from django.http import JsonResponse
from django.db import transaction
class CheckoutView(View):
def post(self, request):
user = request.user
try:
# Step 1: Cart items নাও
cart_items = CartItem.objects.filter(user=user).select_related('product')
if not cart_items.exists():
return JsonResponse({'error': 'Cart খালি!'}, status=400)
# Step 2: Product IDs collect করো
product_ids = [item.product_id for item in cart_items]
# Step 3: ⭐ MAGIC - Sort করো!
product_ids.sort() # ছোট থেকে বড়!
# Step 4: Quantity map বানাও
qty_map = {item.product_id: item.quantity for item in cart_items}
# Step 5: Transaction শুরু করো
with transaction.atomic():
# Step 6: Sorted order এ lock করো
locked_products = {}
for product_id in product_ids:
product = Product.objects.select_for_update().get(id=product_id)
locked_products[product_id] = product
# Step 7: Stock check + update
total_price = Decimal('0')
order_summary = []
for product_id, product in locked_products.items():
quantity = qty_map[product_id]
# Stock check
if product.stock < quantity:
return JsonResponse({
'error': f'{product.name} stock নাই! মাত্র {product.stock}টা আছে'
}, status=400)
# Stock কমাও
product.stock -= quantity
product.save()
# Summary
item_total = product.price * quantity
total_price += item_total
order_summary.append({
'name': product.name,
'quantity': quantity,
'price': str(product.price),
'total': str(item_total)
})
# Step 8: Order তৈরি করো
order = SalesOrder.objects.create(
customer=user,
total_amount=total_price,
status='confirmed'
)
# Order items তৈরি করো
for product_id, product in locked_products.items():
OrderItem.objects.create(
order=order,
product=product,
quantity=qty_map[product_id],
price=product.price
)
# Step 9: Cart খালি করো
cart_items.delete()
# Step 10: Success!
return JsonResponse({
'success': True,
'order_id': order.id,
'message': '🎉 Order সফল হয়েছে!',
'total_price': str(total_price),
'items': order_summary
})
except Product.DoesNotExist:
return JsonResponse({'error': 'Product খুঁজে পাওয়া যায়নি'}, status=404)
except Exception as e:
return JsonResponse({'error': f'Error: {str(e)}'}, status=400)
কেন কাজ করে?
Visual Proof:
রহিমের Cart: [Samsung(5), Mouse(2)]
→ Sort: [2, 5]
→ Lock order: Mouse(2) 🔒 → Samsung(5) 🔒
করিমের Cart: [Mouse(2), Samsung(5)]
→ Sort: [2, 5]
→ Lock order: Mouse(2) 🔒 → Samsung(5) 🔒
Timeline:
0.0s - রহিম: Mouse(2) lock চাইছে...
0.0s - করিম: Mouse(2) lock চাইছে...
0.0s - রহিম: Mouse(2) 🔒 পেয়ে গেল!
0.0s - করিম: ⏳ অপেক্ষা করছে...
0.1s - রহিম: Samsung(5) 🔒 lock করলো
0.2s - রহিম: ✅ Order complete! 🔓 Unlock
0.2s - করিম: Mouse(2) 🔒 পেল!
0.3s - করিম: Samsung(5) 🔒 lock করলো
0.4s - করিম: ✅ Order complete! 🔓 Unlock
Result: কোনো Deadlock নাই! দুইটাই সফল! 🎉
✅ সমাধান ৪: Nowait Strategy – Retry Friendly
যদি lock পাও না, অপেক্ষা না করে user কে বলে দাও!
Retry Code:
class RetryCheckoutView(View):
def post(self, request):
user = request.user
try:
cart_items = CartItem.objects.filter(user=user)
product_ids = sorted([item.product_id for item in cart_items])
with transaction.atomic():
# nowait=True মানে: Lock না পেলে সাথে সাথে Error!
locked_products = {}
for product_id in product_ids:
product = Product.objects.select_for_update(
nowait=True # ⭐ Key difference!
).get(id=product_id)
locked_products[product_id] = product
# বাকি কাজ আগের মতো...
# Stock check + update
return JsonResponse({'success': True})
except DatabaseError:
# Lock পাইনি! User কে বলো retry করতে
return JsonResponse({
'error': 'খুব Busy! একটু পরে চেষ্টা করো',
'retry': True
}, status=409)
সুবিধা:
- Deadlock হবে না (অপেক্ষা করে না)
- User friendly (retry করতে পারে)
- High traffic এর জন্য ভালো
✅ সমাধান ৫: Optimistic Locking – Version Field
Advanced technique! Product এ version field রাখো।
Model with Version:
class ProductWithVersion(models.Model):
name = models.CharField(max_length=200)
stock = models.IntegerField(default=0)
version = models.IntegerField(default=0) # ⭐ Version tracking
View with Optimistic Lock:
class OptimisticCheckoutView(View):
def post(self, request):
user = request.user
product_id = request.POST.get('product_id')
quantity = int(request.POST.get('quantity', 1))
max_retries = 3
for attempt in range(max_retries):
try:
with transaction.atomic():
# Current version নাও
product = ProductWithVersion.objects.get(id=product_id)
old_version = product.version
if product.stock < quantity:
return JsonResponse({'error': 'Stock নাই!'}, status=400)
# Update শুধু যদি version same থাকে
updated = ProductWithVersion.objects.filter(
id=product_id,
version=old_version # ⭐ Version match?
).update(
stock=F('stock') - quantity,
version=F('version') + 1 # Version বাড়াও
)
if updated:
# Success!
return JsonResponse({
'success': True,
'attempt': attempt + 1
})
else:
# Version mismatch! Retry
continue
except Exception as e:
if attempt == max_retries - 1:
return JsonResponse({'error': str(e)}, status=400)
return JsonResponse({'error': 'খুব busy!'}, status=409)
সব সমাধান একসাথে: Comparison Table
| সমাধান | সহজ? | দ্রুত? | কখন ব্যবহার করবে? |
|---|---|---|---|
| select_for_update() | ✅✅✅ | ✅✅ | সাধারণ ক্ষেত্রে |
| F() Expression | ✅✅ | ✅✅✅ | High traffic |
| Sorting (Multiple) | ✅✅✅ | ✅✅ | Multiple products |
| Nowait + Retry | ✅✅ | ✅✅✅ | Very high traffic |
| Optimistic Lock | ✅ | ✅✅✅ | Advanced cases |
Test করার Code – Copy করে চালাও
Django Shell এ এই code চালাও:
from django.contrib.auth.models import User
from myapp.models import Product, CartItem
from decimal import Decimal
# Users তৈরি করো
rahim = User.objects.create_user('rahim', password='pass123')
karim = User.objects.create_user('karim', password='pass123')
# Products তৈরি করো
iphone = Product.objects.create(
name='iPhone 15',
price=Decimal('120000'),
stock=1, # মাত্র 1টা!
sku='IP15-001'
)
samsung = Product.objects.create(
name='Samsung S24',
price=Decimal('85000'),
stock=2,
sku='SAM-S24'
)
mouse = Product.objects.create(
name='Wireless Mouse',
price=Decimal('1200'),
stock=1,
sku='MOUSE-001'
)
# রহিমের Cart
CartItem.objects.create(user=rahim, product=samsung, quantity=1)
CartItem.objects.create(user=rahim, product=mouse, quantity=1)
# করিমের Cart
CartItem.objects.create(user=karim, product=mouse, quantity=1)
CartItem.objects.create(user=karim, product=samsung, quantity=1)
# এখন দুইটা browser/terminal খুলে একসাথে checkout করো!
Real Production Impact
আমাদের সাইটে যখন এই fix apply করলাম:
Before (Bad Code):
- Response time: 5-10 seconds
- Deadlocks: 10-15 per day
- Failed orders: ~50 per day
- User complaints: প্রচুর!
After (Sorting + F()):
- Response time: 0.2-0.5 seconds (95% improvement!)
- Deadlocks: 0 (zero!)
- Failed orders: 2-3 per day (stock issues only)
- User satisfaction: 📈 significantly improved
Key Takeaways – মনে রাখার মন্ত্র
Single Product Order:
F() Expression ব্যবহার করো = দ্রুত + নিরাপদ
Multiple Products Order:
Product IDs Sort করো = Deadlock মুক্ত
High Traffic Site:
F() + Retry Mechanism = স্থিতিশীল
3 Steps Formula:
- সব Product IDs collect করো
- Sort করো (ছোট থেকে বড়)
- Sorted order এ lock করো
মনে রাখার ছড়া:
"ছোট ID আগে, বড় ID পরে,
Deadlock থাকবে না আর!"
বাচ্চাদের মতো মনে রাখো: দুই ভাইয়ের গল্প
বাসায় দুইটা খেলনা আছে: PlayStation (বড়) আর Phone (ছোট)
❌ খারাপ নিয়ম:
- বড় ভাই: PlayStation নিলাম, Phone চাই
- ছোট ভাই: Phone নিলাম, PlayStation চাই
- দুইজনেই আটকে গেল! 😭
✅ ভালো নিয়ম: “সবাই ছোট জিনিস আগে নিবে!”
- বড় ভাই: Phone → PlayStation → খেললো → রাখলো
- ছোট ভাই: (অপেক্ষা) → Phone → PlayStation → খেললো → রাখলো
- দুইজনেই খেলতে পারলো! 🎉
এটাই Sorting এর magic!
Production Tips – অভিজ্ঞতা থেকে
✅ Do’s:
- সবসময় Product IDs sort করো
- Transaction timeout দাও (5-10 seconds)
- Stock check করার আগে lock করো
- User কে clear message দেখাও
- Error logging করো
❌ Don’ts:
- Random order এ lock করো না
- Transaction এ heavy operations করো না
- Lock hold করে external API call করো না
- Error message generic রাখো না
User Friendly Messages:
Good:
"Samsung S24 stock শেষ! আর মাত্র 0টা আছে।"
Bad:
"Database error occurred"
Performance Optimization
Small Cart (1-3 items):
- select_for_update() + sorting = Perfect!
Medium Cart (4-10 items):
- F() expression + sorting = Better!
Large Cart (10+ items):
- Consider breaking into batches
- Use Celery for async processing
Very High Traffic:
- Redis queue system
- Rate limiting
- CDN caching for product data
Conclusion
Django এ Deadlock এড়ানো আসলে খুব সহজ যদি সঠিক approach নাও:
- Single Product: F() Expression ব্যবহার করো
- Multiple Products: Product IDs sort করো
- High Traffic: Retry mechanism যোগ করো
মনে রাখবে: “ছোট ID আগে, বড় ID পরে – Deadlock থাকবে না আর!”
আশা করি এই tutorial আপনার কাজে লাগবে। কোনো প্রশ্ন থাকলে comment করুন!
Happy Coding! 🚀
Tags: Django, Python, Deadlock, Database, E-commerce, Performance, Concurrency, Race Condition, Transaction, PostgreSQL
Category: Web Development, Django Tutorial
Difficulty: Intermediate
Reading Time: 15 minutes