在多個用戶同時發起對同一個數據提交修改操作時(先查詢,再修改),會出現資源競爭的問題,導致最終修改的數據結果出現異常。
比如限量商品在熱銷時,當多個用戶同時請求購買商品時,最終修改的數據就會出現異常
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(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)]
A操作的結果:goods.stock = 10 - 6 = 4
B操作的結果:goods.stock = 10 - 5 = 5
數據庫樂觀鎖:
樂觀鎖並不是真實存在的鎖,而是在更新的時候判斷此時的庫存是否是之前查詢出的庫存,如果相同,表示沒人修改,可以更新庫存,否則表示別人搶過資源,不再執行庫存更新。類似如下操作
使用原生的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')