程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式師世界 >> 編程語言 >  >> 更多編程語言 >> Python

django中實現悲觀鎖與樂觀鎖

編輯:Python

django中實現悲觀鎖與樂觀鎖

1.數據庫並發處理問題

在多個用戶同時發起對同一個數據提交修改操作時(先查詢,再修改),會出現資源競爭的問題,導致最終修改的數據結果出現異常。

比如限量商品在熱銷時,當多個用戶同時請求購買商品時,最終修改的數據就會出現異常

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-RWpbej13-1636196467076)(2018082423563121)]

實例一:

數據庫模型:

from django.db import models
class GoodsInfo(models.Model):
""" 商品 """
name = models.CharField(max_length=50, verbose_name='名稱')
stock = models.IntegerField(default=0, verbose_name='庫存')
class Meta:
db_table = 'tb_goodsinfo'

視圖:

from django.http import HttpResponse
from rest_framework.generics import GenericAPIView
from app01.models import GoodsInfo
class Goods(GenericAPIView):
""" 購買商品 """
def post(self, request):
# 獲取請求頭中查詢字符串數據
goods_id = request.GET.get('goods_id')
count = int(request.GET.get('count'))
# 查詢商品對象
goods = GoodsInfo.objects.filter(id=goods_id).first()
# 獲取原始庫存
origin_stock = goods.stock
# 判斷商品庫存是否充足
if origin_stock < count:
return HttpResponse(content="商品庫存不足", status=400)
# 演示多個用戶並發請求
import time
time.sleep(5)
# 減少商品的庫存數量,保存到數據庫
goods.stock = origin_stock - count
goods.save()
return HttpResponse(content="操作成功", status=200)

我們來使用兩個postman模擬A,B用戶同時請求,用戶A買6套商品,用戶B買5套商品

運行結果:

  • 輸出日志:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-iKs6oNW9-1636196467078)(20180825024104689)]

  • 查詢數據庫:

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-dKwicobl-1636196467080)(20180825024359382)]

  • 兩個postman發出的post請求均提示 “操作成功”

2、分析及結論:

  • 當A用戶請求的時候,goods.stock = origin_stock - count

​ A操作的結果:goods.stock = 10 - 6 = 4

  • 可是B用戶判斷庫存的時候,A還未將修改的數據保存到數據庫,所以B獲取的庫存數量也是 10

​ B操作的結果:goods.stock = 10 - 5 = 5

  • 寫入數據庫操作中,B的數據將A的數據覆蓋,故最後的庫存還是 5

3、解決方法

  • 思路一:

數據庫樂觀鎖:
樂觀鎖並不是真實存在的鎖,而是在更新的時候判斷此時的庫存是否是之前查詢出的庫存,如果相同,表示沒人修改,可以更新庫存,否則表示別人搶過資源,不再執行庫存更新。類似如下操作

使用原生的SQL語句
update tb_goodsinfo set stock=5 where id=1 and stock=10;
使用Django中的語法
GoodsInfo.objects.filter(id=1, stock=10).update(stock=5)
# GoodsInfo:模型類, id:商品id, stock:庫存

改寫視圖:

from django.http import HttpResponse
from rest_framework.generics import GenericAPIView
from app01.models import GoodsInfo
class Goods(GenericAPIView):
""" 購買商品 """
def post(self, request):
# 獲取請求頭中查詢字符串數據
goods_id = request.GET.get('goods_id')
count = int(request.GET.get('count'))
while True:
# 查詢商品對象
goods = GoodsInfo.objects.filter(id=goods_id).first()
# 獲取原始庫存
origin_stock = goods.stock
# 判斷商品庫存是否充足
if origin_stock < count:
return HttpResponse(content="商品庫存不足", status=400)
# 演示並發請求
import time
time.sleep(5)
# 減少商品的庫存數量,保存到數據庫
# goods.stock = origin_stock - count
# goods.save()
""" 使用樂觀鎖進行處理,一步完成數據庫的查詢和更新 """
# update返回受影響的行數
result = GoodsInfo.objects.filter(id=goods.id, stock=origin_stock).update(stock=origin_stock - count)
if result == 0:
# 表示更新失敗,有人搶先購買了商品,重新獲取庫存信息,判斷庫存
continue
# 表示購買成功,退出 while 循環
break
return HttpResponse(content="操作成功", status=200)

結果:

A用戶返回 “操作成功”, B用戶返回 “商品庫存不足”

  • 思路二:

使用數據版本(Version)記錄機制實現,這是樂觀鎖最常用的一種實現方式。何謂數據版本?即為數據增加一個版本標識,一般是通過為數據庫表增加一個數字類型的 “version” 字段來實現。當讀取數據時,將version字段的值一同讀出,數據每更新一次,對此version值加一。當我們提交更新的時候,判斷數據庫表對應記錄的當前版本信息與第一次取出來的version值進行比對,如果數據庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期數據

# versionValue為獲取的版本號
result = GoodsInfo.objects.filter(id=goods.id, stock=versionValue).update(stock=origin_stock - count)

數據庫悲觀鎖

概念:

總是假設最壞的情況,每次取數據時都認為其他線程會修改,所以都會加鎖(讀鎖、寫鎖、行鎖等)

當其他線程想要訪問數據時,都需要阻塞掛起。可以依靠數據庫實現,如行鎖、讀鎖和寫鎖等,都是在操作之前加鎖

保證同一時刻只有一個線程能操作數據,其他線程則會被 block

運用場景:

▧ 無髒讀 上鎖數據保證一致, 因此無髒讀, 對髒讀不允許的環境悲觀鎖可以勝任

▧ 無並行 悲觀鎖對事務成功性可以保證, 但是會對數據加鎖導致無法實現數據的並行處理.

▧ 事務成功率高 上鎖保證一次成功, 因此在對數據處理的成功率要求較高的時候更適合悲觀鎖.

▧ 開銷大 悲觀鎖的上鎖解鎖是有開銷的, 如果超大的並發量這個開銷就不容小視, 因此不適合在高並發環境中使用悲觀鎖

▧ 一次性完成 如果樂觀鎖多次嘗試的代價比較大,也建議使用悲觀鎖, 悲觀鎖保證一次成功

代碼

select_for_update()`這個方法有兩個默認參數,`nowait=False`和`skip_locked=False

nowait的含義是匹配的記錄被鎖時不等待,會拋異常。但是 MySQL8.0 以前不支持。

skip_locked的含義是SELECT時跳過被鎖的記錄。

select_for_update()方法必須應用在事務中,可利用@transaction.atomic()裝飾器包裹視圖函數

from django.shortcuts import render
from django.http import HttpResponse
from django.views.generic import View
from django.db import transaction
from 應用名.models import 模型類名
# 類視圖 (並發,悲觀鎖)
class MyView(View):
@transaction.atomic
def post(self, request):
# select * from 表名 where id=1 for update; 
# for update 就表示鎖,只有獲取到鎖才會執行查詢,否則阻塞等待。
obj = 模型類名.objects.select_for_update().get(id=1)
# 等事務提交後,會自動釋放鎖。
return HttpResponse('ok')

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式師世界 All Rights Reserved