Django 面試準備 08-1:庫存扣減問題
深入理解高並發場景下的庫存扣減與解決方案
目錄
08-1. 庫存扣減問題
📌 問題場景
在電商系統中,當多個用戶同時購買同一商品時,如何確保庫存扣減的正確性?
典型場景
# 商品庫存為 10
# 用戶 A 購買 5 件
# 用戶 B 購買 6 件
# 同時請求,會發生什麼?🔴 錯誤示範:天真的實現
# ❌ 有問題的代碼
from django.db import models
from django.http import JsonResponse
class Product(models.Model):
name = models.CharField(max_length=200)
stock = models.IntegerField(default=0) # 庫存數量
def purchase(request, product_id, quantity):
# 1. 查詢商品
product = Product.objects.get(id=product_id)
# 2. 檢查庫存
if product.stock >= quantity:
# 3. 扣減庫存
product.stock -= quantity
product.save()
return JsonResponse({'status': 'success'})
else:
return JsonResponse({'status': 'insufficient_stock'})⚠️ 問題分析
時間線演示
時刻 用戶 A 用戶 B 庫存
T1 讀取庫存 = 10
T2 讀取庫存 = 10 10
T3 檢查:10 >= 5 ✓
T4 檢查:10 >= 6 ✓ 10
T5 計算:10 - 5 = 5
T6 計算:10 - 6 = 4 10
T7 保存庫存 = 5 5
T8 保存庫存 = 4 4 ⚠️
結果:庫存從 10 變成 4,但實際賣出 11 件(5 + 6)!這就是典型的 競態條件(Race Condition)。
✅ 解決方案 1:數據庫級別的原子更新
使用 F() 表達式
from django.db.models import F
from django.db import transaction
def purchase_v1(request, product_id, quantity):
"""
使用 F() 表達式在數據庫層面進行原子操作
"""
with transaction.atomic():
# 直接在數據庫層面進行扣減
affected_rows = Product.objects.filter(
id=product_id,
stock__gte=quantity # 確保庫存充足
).update(
stock=F('stock') - quantity
)
if affected_rows > 0:
return JsonResponse({'status': 'success'})
else:
return JsonResponse({'status': 'insufficient_stock'})🔍 工作原理
生成的 SQL:
UPDATE product
SET stock = stock - 5
WHERE id = 123 AND stock >= 5;這條 SQL 是原子操作,數據庫會:
- 加鎖
- 檢查條件
- 更新數據
- 釋放鎖
一氣呵成,不會被打斷。
✅ 解決方案 2:悲觀鎖(Pessimistic Locking)
def purchase_v2(request, product_id, quantity):
"""
使用 select_for_update 實現悲觀鎖
"""
with transaction.atomic():
# 鎖定這一行,其他事務必須等待
product = Product.objects.select_for_update().get(id=product_id)
if product.stock >= quantity:
product.stock -= quantity
product.save()
return JsonResponse({'status': 'success'})
else:
return JsonResponse({'status': 'insufficient_stock'})🔍 工作原理
-- PostgreSQL/MySQL
SELECT * FROM product WHERE id = 123 FOR UPDATE;FOR UPDATE 會:
- 鎖定該行記錄
- 其他事務必須等待當前事務完成
- 避免並發修改
⚖️ 優缺點
優點:
- 簡單直觀
- 確保數據一致性
缺點:
- 性能較低(等待鎖)
- 高並發下容易形成鎖競爭
- 可能產生死鎖
✅ 解決方案 3:樂觀鎖(Optimistic Locking)
class Product(models.Model):
name = models.CharField(max_length=200)
stock = models.IntegerField(default=0)
version = models.IntegerField(default=0) # 版本號
def purchase_v3(request, product_id, quantity):
"""
使用版本號實現樂觀鎖
"""
max_retries = 3
for attempt in range(max_retries):
try:
with transaction.atomic():
product = Product.objects.get(id=product_id)
old_version = product.version
# 檢查庫存
if product.stock < quantity:
return JsonResponse({'status': 'insufficient_stock'})
# 嘗試更新(帶版本檢查)
affected_rows = Product.objects.filter(
id=product_id,
version=old_version,
stock__gte=quantity
).update(
stock=F('stock') - quantity,
version=F('version') + 1
)
if affected_rows > 0:
return JsonResponse({'status': 'success'})
# 版本不匹配,說明被其他事務修改了,重試
continue
except Exception as e:
if attempt == max_retries - 1:
return JsonResponse({'status': 'error', 'message': str(e)})
return JsonResponse({'status': 'retry_exceeded'})🔍 工作原理
用戶 A: 用戶 B:
讀取:stock=10, version=1 讀取:stock=10, version=1
扣減 5 件 扣減 6 件
更新 WHERE version=1 ✓ 更新 WHERE version=1 ✗ (version已經變成2)
version -> 2 重試...
讀取:stock=5, version=2
檢查失敗(庫存不足)✓⚖️ 優缺點
優點:
- 無需加鎖,性能好
- 適合讀多寫少的場景
缺點:
- 需要重試機制
- 高並發下重試次數多
📊 方案對比
| 方案 | 並發性能 | 實現複雜度 | 適用場景 |
|---|---|---|---|
| F() 表達式 | ⭐⭐⭐⭐⭐ | ⭐ | 簡單扣減操作 |
| 悲觀鎖 | ⭐⭐⭐ | ⭐⭐ | 對一致性要求極高 |
| 樂觀鎖 | ⭐⭐⭐⭐ | ⭐⭐⭐ | 衝突較少的場景 |
🎯 實戰建議
1. 首選 F() 表達式
對於簡單的庫存扣減,F() 表達式是最優解:
# 推薦寫法
Product.objects.filter(
id=product_id,
stock__gte=quantity
).update(stock=F('stock') - quantity)2. 添加監控和日誌
import logging
logger = logging.getLogger(__name__)
def purchase_with_logging(request, product_id, quantity):
with transaction.atomic():
affected_rows = Product.objects.filter(
id=product_id,
stock__gte=quantity
).update(stock=F('stock') - quantity)
if affected_rows > 0:
logger.info(f'Product {product_id} stock reduced by {quantity}')
return JsonResponse({'status': 'success'})
else:
logger.warning(f'Product {product_id} insufficient stock')
return JsonResponse({'status': 'insufficient_stock'})3. 考慮業務邏輯
def purchase_complete(request, product_id, quantity):
"""
完整的購買流程
"""
with transaction.atomic():
# 1. 扣減庫存(使用 F() 表達式)
affected_rows = Product.objects.filter(
id=product_id,
stock__gte=quantity
).update(stock=F('stock') - quantity)
if affected_rows == 0:
return JsonResponse({'status': 'insufficient_stock'})
# 2. 創建訂單
order = Order.objects.create(
product_id=product_id,
quantity=quantity,
user=request.user
)
# 3. 扣減用戶餘額等其他操作...
return JsonResponse({
'status': 'success',
'order_id': order.id
})💡 面試要點
Q1: 為什麼會出現庫存超賣?
答: 因為 讀取-檢查-更新 這三個步驟不是原子操作,在高並發下會出現競態條件。
Q2: F() 表達式為什麼能解決問題?
答: F() 表達式在數據庫層面執行 UPDATE stock = stock - N WHERE stock >= N,這是一條原子 SQL,避免了應用層的競態條件。
Q3: 悲觀鎖和樂觀鎖的區別?
答:
- 悲觀鎖:假設會發生衝突,提前加鎖(
SELECT FOR UPDATE) - 樂觀鎖:假設不會發生衝突,通過版本號檢測衝突後重試
Q4: 高並發場景如何選擇方案?
答:
- 簡單扣減 → F() 表達式
- 複雜業務邏輯 → 悲觀鎖(select_for_update)
- 讀多寫少 → 樂觀鎖(版本號)
- 超高並發 → 分布式鎖 + Redis
🔗 下一篇
在下一篇文章中,我們將探討 秒殺系統設計,學習如何在超高並發場景下設計一個完整的秒殺系統。
閱讀時間:12 分鐘