python 如何解決高併發下的庫存問題
一個簡單的使用場景:一件商品的庫存只有5件,同時A使用者買了5個,B使用者買了5個,都提交資料,照成庫存不足的問題。
邏輯:根據一般電商商品的模型類,生成訂單一般包括訂單類(Order)和訂單詳情類(DetailOrder),這兩張表根據外來鍵order_id 進行關聯,所以是同生共死的關係,所以我們在這裡用事務來控制。那麼python如何解決庫存問題呢?
python 提供了2種方法解決該問題的問題:1,悲觀鎖;2,樂觀鎖
悲觀鎖:在查詢商品儲存的時候加鎖 select_for_update() 在發生事務的commit或者是事務的rollback時,自動釋放該鎖,這樣其他使用者就可以接著查詢該商品。
樂觀鎖:樂觀鎖不是真正的鎖,在建立訂單之前查詢商品的庫存,在建立訂單詳情表前,update更新查詢資料,如果兩次查詢的庫存量一樣就建立詳情表,並減去庫存,否則,迴圈三次,如果都不一樣,就發生rollback。
使用場景:併發量高的時候使用悲觀鎖,缺點:加鎖消耗資源
併發量低的時候使用樂觀鎖,缺點:樂觀鎖迴圈耗費時間。
程式碼:
悲觀鎖:
@transaction.atomic
def post(self, request):
"""接受資料 修改購物車資料 新增訂單 和訂單商品 加悲觀鎖 解決併發問題"""
sku_ids = request.POST.get('sku_ids')
addr_id =request.POST.get('addr_id')
pay_method = request.POST.get('pay_method')
transit_price = request.POST.get('transit_price')
user = request.user
if not user.is_authenticated(): # 判斷是否登陸
return JsonResponse({'res': 0, 'errmsg': '使用者未登入,請先登陸'})
try: # 判斷地址是否存在
address = Customer.objects.get(id=addr_id)
except Customer.DoesNotExist:
return JsonResponse({'res': 1, 'errmsg': '地址不存在'})
# 判斷支付方式是否存在
if pay_method not in OrderInfo.PAY_METHODS.keys():
return JsonResponse({'res': 2, 'errmsg': '支付方式不存在'})
total_price = 0
total_count = 0
order_id = datetime.now().strftime('%Y%m%d%H%M%S') + str(user.id)
# 設定事務儲存點
tip = transaction.savepoint()
try:
new_order =OrderInfo.objects.create(order_id=order_id,
user=user,
addr=address,
pay_method=pay_method,
total_count=total_count,
total_price=total_price,
transit_price=transit_price)
except Exception as e:
# 回滾到tip
transaction.savepoint_rollback(tip)
return JsonResponse({'res': 3, 'errmsg': '生成訂單失敗'})
cart_key = 'cart_%d' % user.id
conn = get_redis_connection('default')
skus = conn.hgetall(cart_key)
for sku_id in eval(sku_ids): # 迴圈獲得商品的資訊 每條資料生成一個訂單商品資料
# 獲得購物車中每個商品的數量
sku_count = conn.hget(cart_key, sku_id)
if not sku_count: # 判斷購物車裡有沒有該商品
# 回滾到tip
transaction.savepoint_rollback(tip)
return JsonResponse({'res': 4, 'errmsg': '該商品不在購物車中'})
try: # 在商品表中 找該商品sel
sku = GoodsSKU.objects.select_for_update().get(id=sku_id, is_show=1)
except GoodsSKU.DoesNotExist:
# 回滾到tip
transaction.savepoint_rollback(tip)
return JsonResponse({'res': 5, 'errmsg': '該商品已下架'})
price = sku.price
sku_count = int(sku_count)
if sku_count > int(sku.count): # 判斷庫存
# 回滾到tip
transaction.savepoint_rollback(tip)
return JsonResponse({'res': 6, 'errmsg': '庫存不足'})
price = int(price)
sku_price = sku_count * price
total_price += sku_price
total_count += sku_count
# 新增訂單商品表
try:
OrderGoods.objects.create(order=new_order,
sku=sku,
count=sku_count,
price=sku_price,
)
except Exception as e:
# 回滾到tip
transaction.savepoint_rollback(tip)
return JsonResponse({'res': 7, 'errmsg': '訂單商品建立失敗'})
# 減少商品庫存 增加商品銷售額
sku.count -= sku_count
sku.sale_count += sku_count
sku.save()
# 刪除修改購物車資料
conn.hdel(cart_key, sku_id)
total_price += int(transit_price)
# 修改order 訂單數量 總價 郵費
new_order.total_count = total_count
new_order.total_price = total_price
new_order.transit_price = transit_price
new_order.save()
#釋放儲存點
transaction.savepoint_commit(tip)
樂觀鎖:
@transaction.atomic
def post(self, request):
'''訂單建立'''
# 判斷使用者是否登入
user = request.user
if not user.is_authenticated():
return JsonResponse({'res':0, 'errmsg':'使用者未登入'})
# 接收引數
addr_id = request.POST.get('addr_id')
pay_method = request.POST.get('pay_method')
sku_ids = request.POST.get('sku_ids') # 1,4
# 校驗引數
if not all([addr_id, pay_method, sku_ids]):
return JsonResponse({'res':1, 'errmsg':'資料不完整'})
# 校驗地址
try:
addr = Address.objects.get(id=addr_id)
except Address.DoesNotExist:
# 地址不存在
return JsonResponse({'res':2, 'errmsg':'地址資訊錯誤'})
# 校驗支付方式
if pay_method not in OrderInfo.PAY_METHODS.keys():
# 支付方式無效
return JsonResponse({'res':3, 'errmsg':'支付方式無效'})
# 業務處理:訂單建立
# 組織引數
# 訂單id(order_id): 20171211123130+使用者的id
order_id = datetime.now().strftime('%Y%m%d%H%M%S')+str(user.id)
# 運費
transit_price = 10
# 總數目和總金額
total_count = 0
total_price = 0
# 設定儲存點
sid = transaction.savepoint()
try:
# todo: 向df_order_info表中新增一條記錄
order = OrderInfo.objects.create(order_id=order_id,
user=user,
addr=addr,
pay_method=pay_method,
total_count=total_count,
total_price=total_price,
transit_price=transit_price)
conn = get_redis_connection('default')
cart_key = 'cart_%d'%user.id
# todo: 使用者的訂單中包含幾個商品,就應該向df_order_goods中新增幾條記錄
sku_ids = sku_ids.split(',') # [1,4]
for sku_id in sku_ids:
for i in range(3):
# 根據sku_id獲取商品的資訊
try:
# select * from df_goods_sku where id=sku_id;
sku = GoodsSKU.objects.get(id=sku_id)
except GoodsSKU.DoesNotExist:
transaction.savepoint_rollback(sid)
return JsonResponse({'res':4, 'errmsg':'商品資訊錯誤'})
# 獲取使用者要購買的商品的數目
count = conn.hget(cart_key, sku_id)
# todo: 判斷商品庫存
if int(count) > sku.stock:
transaction.savepoint_rollback(sid)
return JsonResponse({'res':6, 'errmsg':'商品庫存不足'})
# todo: 減少商品的庫存,增加銷量
origin_stock = sku.stock
new_stock = origin_stock - int(count)
new_sales = sku.sales + int(count)
# print('user:%d i:%d stock:%d'%(user.id, i, origin_stock))
# import time
# time.sleep(10)
# 返回更新的行數
# update df_goods_sku set stock=new_stock, sales=new_sales
# where id=sku_id and stock=origin_stock
res = GoodsSKU.objects.filter(id=sku_id, stock=origin_stock).update(stock=new_stock, sales=new_sales)
if res == 0:
# 更新失敗
if i == 2:
# 已經嘗試了3次,下單失敗
transaction.savepoint_rollback(sid)
return JsonResponse({'res':7, 'errmsg':'下單失敗2'})
continue
# todo: 向df_order_goods中新增一條記錄
OrderGoods.objects.create(order=order,
sku=sku,
count=count,
price=sku.price)
# todo: 累加計算商品的總件數和總金額
total_count += int(count)
total_price += sku.price*int(count)
# 更新成功之後跳出迴圈
break
# todo: 更新order的total_count和total_price
order.total_count = total_count
order.total_price = total_price
order.save()
except Exception as e:
transaction.savepoint_rollback(sid)
return JsonResponse({'res':7, 'errmsg':'下單失敗'})
# 釋放儲存點
transaction.savepoint_commit(sid)
# todo: 刪除購物車對應記錄資訊
conn.hdel(cart_key, *sku_ids) # 1,4
# 返回應答
return JsonResponse({'res':5, 'message':'訂單建立成功'})
tip:使用樂觀鎖時,記得修改mysql的隔離級別:vi /etc/mysql/mysql.conf.d/mysqld.cnf 在mysqld最下面一行新增:transaction-isolation = READ_COMMITTED 。