基于Django的樂觀鎖與悲觀鎖解決訂單并發(fā)問題詳解
前言
訂單并發(fā)這個問題我想大家都是有一定認(rèn)識的,這里我說一下我的一些淺見,我會盡可能的讓大家了解如何解決這類問題。
在解釋如何解決訂單并發(fā)問題之前,需要先了解一下什么是數(shù)據(jù)庫的事務(wù)。(我用的是mysql數(shù)據(jù)庫,這里以mysql為例)
1) 事務(wù)概念
一組mysql語句,要么執(zhí)行,要么全不不執(zhí)行。
2) mysql事務(wù)隔離級別
Read Committed(讀取提交內(nèi)容)
如果是Django2.0以下的版本,需要去修改到這個隔離級別,不然樂觀鎖操作時無法讀取已經(jīng)被修改的數(shù)據(jù)
RepeatableRead(可重讀)
這是這是Mysql默認(rèn)的隔離級別,可以到mysql的配置文件中去修改;
transcation-isolation = READ-COMMITTED
在mysql配置文件中添加這行然后重啟mysql就可以將事務(wù)隔離級別修改至Read Committed
其他事務(wù)知識這里不會用到就不浪費(fèi)時間去做介紹了。
悲觀鎖:開啟事務(wù),然后給mysql的查詢語句最后加上for update。
這是在干什么呢。可能大家有些不理解,其實(shí)就是給資源加上和多線程中加互斥鎖一樣的東西,確保在一個事務(wù)結(jié)束之前,別的事務(wù)無法對該數(shù)據(jù)進(jìn)行操作。
下面是悲觀鎖的代碼,加鎖和解鎖都是需要消耗CPU資源的,所以在訂單并發(fā)少的情況使用樂觀鎖會是一個更好的選擇。
class OrderCommitView(View): """悲觀鎖""" # 開啟事務(wù)裝飾器 @transaction.atomic def post(self,request): """訂單并發(fā) ———— 悲觀鎖""" # 拿到商品id goods_ids = request.POST.getlist('goods_ids') # 校驗(yàn)參數(shù) if len(goods_ids) == 0 : return JsonResponse({'res':0,'errmsg':'數(shù)據(jù)不完整'}) # 當(dāng)前時間字符串 now_str = datetime.now().strftime('%Y%m%d%H%M%S') # 訂單編號 order_id = now_str + str(request.user.id) # 地址 pay_method = request.POST.get('pay_method') # 支付方式 address_id = request.POST.get('address_id') try: address = Address.objects.get(id=address_id) except Address.DoesNotExist: return JsonResponse({'res':1,'errmsg':'地址錯誤'}) # 商品數(shù)量 total_count = 0 # 商品總價 total_amount = 0 # 獲取redis連接 conn = get_redis_connection('default') # 拼接key cart_key = 'cart_%d' % request.user.id # # 創(chuàng)建保存點(diǎn) sid = transaction.savepoint() order_info = OrderInfo.objects.create( order_id = order_id, user = request.user, addr = address, pay_method = pay_method, total_count = total_count, total_price = total_amount ) for goods_id in goods_ids: # 嘗試查詢商品 # 此處考慮訂單并發(fā)問題, try: # goods = Goods.objects.get(id=goods_id) # 不加鎖查詢 goods = Goods.objects.select_for_update().get(id=goods_id) # 加互斥鎖查詢 except Goodsgoods.DoesNotExist: # 回滾到保存點(diǎn) transaction.rollback(sid) return JsonResponse({'res':2,'errmsg':'商品信息錯誤'}) # 取出商品數(shù)量 count = conn.hget(cart_key,goods_id) if count is None: # 回滾到保存點(diǎn) transaction.rollback(sid) return JsonResponse({'res':3,'errmsg':'商品不在購物車中'}) count = int(count) if goods.stock < count: # 回滾到保存點(diǎn) transaction.rollback(sid) return JsonResponse({'res':4,'errmsg':'庫存不足'}) # 商品銷量增加 goods.sales += count # 商品庫存減少 goods.stock -= count # 保存到數(shù)據(jù)庫 goods.save() OrderGoods.objects.create( order = order_info, goods = goods, count = count, price = goods.price ) # 累加商品件數(shù) total_count += count # 累加商品總價 total_amount += (goods.price) * count # 更新訂單信息中的商品總件數(shù) order_info.total_count = total_count # 更新訂單信息中的總價格 order_info.total_price = total_amount + order_info.transit_price order_info.save() # 事務(wù)提交 transaction.commit() return JsonResponse({'res':5,'errmsg':'訂單創(chuàng)建成功'})
然后就是樂觀鎖查詢了,相比悲觀鎖,樂觀鎖其實(shí)并不能稱為是鎖,那么它是在做什么事情呢。
其實(shí)是在你要進(jìn)行數(shù)據(jù)庫操作時先去查詢一次數(shù)據(jù)庫中商品的庫存,然后在你要更新數(shù)據(jù)庫中商品庫存時,將你一開始查詢到的庫存數(shù)量和商品的ID一起作為更新的條件,當(dāng)受影響行數(shù)返回為0時,說明沒有修改成功,那么就是說別的進(jìn)程修改了該數(shù)據(jù),那么你就可以回滾到之前沒有進(jìn)行數(shù)據(jù)庫操作的時候,重新查詢,重復(fù)之前的操作一定次數(shù),如果超過你設(shè)置的次數(shù)還是不能修改那么就直接返回錯誤結(jié)果。
該方法只適用于訂單并發(fā)較少的情況,如果失敗次數(shù)過多,會帶給用戶不良體驗(yàn),同時適用該方法要注意數(shù)據(jù)庫的隔離級別一定要設(shè)置為Read Committed 。
最好在使用樂觀鎖之前查看一下數(shù)據(jù)庫的隔離級別,mysql中查看事物隔離級別的命令為
select @@global.tx_isolation;
class OrderCommitView(View): """樂觀鎖""" # 開啟事務(wù)裝飾器 @transaction.atomic def post(self,request): """訂單并發(fā) ———— 樂觀鎖""" # 拿到id goods_ids = request.POST.get('goods_ids') if len(goods_ids) == 0 : return JsonResponse({'res':0,'errmsg':'數(shù)據(jù)不完整'}) # 當(dāng)前時間字符串 now_str = datetime.now().strftime('%Y%m%d%H%M%S') # 訂單編號 order_id = now_str + str(request.user.id) # 地址 pay_method = request.POST.get('pay_method') # 支付方式 address_id = request.POST.get('address_id') try: address = Address.objects.get(id=address_id) except Address.DoesNotExist: return JsonResponse({'res':1,'errmsg':'地址錯誤'}) # 商品數(shù)量 total_count = 0 # 商品總價 total_amount = 0 # 訂單運(yùn)費(fèi) transit_price = 10 # 創(chuàng)建保存點(diǎn) sid = transaction.savepoint() order_info = OrderInfo.objects.create( order_id = order_id, user = request.user, addr = address, pay_method = pay_method, total_count = total_count, total_price = total_amount, transit_price = transit_price ) # 獲取redis連接 goods = get_redis_goodsection('default') # 拼接key cart_key = 'cart_%d' % request.user.id for goods_id in goods_ids: # 嘗試查詢商品 # 此處考慮訂單并發(fā)問題, # redis中取出商品數(shù)量 count = goods.hget(cart_key, goods_id) if count is None: # 回滾到保存點(diǎn) transaction.savepoint_rollback(sid) return JsonResponse({'res': 3, 'errmsg': '商品不在購物車中'}) count = int(count) for i in range(3): # 若存在訂單并發(fā)則嘗試下單三次 try: goods = Goodsgoods.objects.get(id=goods_id) # 不加鎖查詢 # goods = Goodsgoods.objects.select_for_update().get(id=goods_id) # 加互斥鎖查詢 except Goodsgoods.DoesNotExist: # 回滾到保存點(diǎn) transaction.savepoint_rollback(sid) return JsonResponse({'res':2,'errmsg':'商品信息錯誤'}) origin_stock = goods.stock print(origin_stock, 'stock') print(goods.id, 'id') if origin_stock < count: # 回滾到保存點(diǎn) transaction.savepoint_rollback(sid) return JsonResponse({'res':4,'errmsg':'庫存不足'}) # # 商品銷量增加 # goods.sales += count # # 商品庫存減少 # goods.stock -= count # # 保存到數(shù)據(jù)庫 # goods.save() # 如果下單成功后的庫存 new_stock = goods.stock - count new_sales = goods.sales + count res = Goodsgoods.objects.filter(stock=origin_stock,id=goods_id).update(stock=new_stock,sales=new_sales) print(res) if res == 0: if i == 2: # 回滾 transaction.savepoint_rollback(sid) return JsonResponse({'res':5,'errmsg':'下單失敗'}) continue else: break OrderGoods.objects.create( order = order_info, goods = goods, count = count, price = goods.price ) # 刪除購物車中記錄 goods.hdel(cart_key,goods_id) # 累加商品件數(shù) total_count += count # 累加商品總價 total_amount += (goods.price) * count # 更新訂單信息中的商品總件數(shù) order_info.total_count = total_count # 更新訂單信息中的總價格 order_info.total_price = total_amount + order_info.transit_price order_info.save() # 事務(wù)提交 transaction.savepoint_commit(sid) return JsonResponse({'res':6,'errmsg':'訂單創(chuàng)建成功'})
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Python爬蟲實(shí)現(xiàn)爬取百度百科詞條功能實(shí)例
這篇文章主要介紹了Python爬蟲實(shí)現(xiàn)爬取百度百科詞條功能,結(jié)合完整實(shí)例形式分析了Python爬蟲的基本原理及爬取百度百科詞條的步驟、網(wǎng)頁下載、解析、數(shù)據(jù)輸出等相關(guān)操作技巧,需要的朋友可以參考下2019-04-04Python實(shí)現(xiàn)常見坐標(biāo)系的相互轉(zhuǎn)換
WGS84坐標(biāo)系、GCJ02坐標(biāo)系、BD09坐標(biāo)系和Web?墨卡托投影坐標(biāo)系是我們常見的四個坐標(biāo)系。這篇文章為大家整理了這四個坐標(biāo)系之間相互轉(zhuǎn)換的方法,需要的可以參考一下2023-02-02深入理解Python虛擬機(jī)中魔術(shù)方法的使用
這篇文章主要給大家介紹在?cpython?當(dāng)中一些比較花里胡哨的魔術(shù)方法,以幫助我們自己實(shí)現(xiàn)比較花哨的功能,當(dāng)然這其中也包含一些也非常實(shí)用的魔術(shù)方法,需要的可以參考下2023-05-05Python機(jī)器學(xué)習(xí)庫scikit-learn使用詳解
scikit-learn是Python中最流行的機(jī)器學(xué)習(xí)庫之一,它提供了各種各樣的機(jī)器學(xué)習(xí)算法和工具,包括分類、回歸、聚類、降維等2023-03-03Python 閉包,函數(shù)分隔作用域,nonlocal聲明非局部變量操作示例
這篇文章主要介紹了Python 閉包,函數(shù)分隔作用域,nonlocal聲明非局部變量操作,結(jié)合實(shí)例形式分析了Python閉包及閉包中的變量聲明相關(guān)操作技巧,需要的朋友可以參考下2019-10-10