python資料庫併發處理(樂觀鎖)
1.資料庫併發處理問題
在多個使用者同時發起對同一個資料提交修改操作時(先查詢,再修改),會出現資源競爭的問題,導致最終修改的資料結果出現異常。
比如限量商品在熱銷時,當多個使用者同時請求購買商品時,最終修改的資料就會出現異常
下面我們來寫點程式碼還原一下現象:
1.新建專案Optimistic locking,建立應用app01,編輯models建立一張表並執行資料庫遷移,如下:
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'
2.往資料庫中插入一條資料:insert into tb_goodsinfo values(0, "macbook", 10);
3.定義Goods檢視類,
- 增加判斷庫存和修改庫存之間的間隙,就可以模擬出A使用者尚未修改庫存之前,B使用者已經開始進行判斷庫存,導致誤差:
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)
from django.conf.urls import url
from . import views
urlpatterns =[
url(r'^goods/$', views.Goods.as_view()),
]
我們先使用postman來模擬單個使用者請求
- 再來查詢資料庫,單個使用者請求正常,(將stock恢復到10)
模擬多個使用者請求
我們來使用兩個postman模擬A,B使用者同時請求,使用者A買6套商品,使用者B買5套商品
執行結果:
- 輸出日誌:
- 查詢資料庫:
- 兩個postman發出的post請求均提示 “操作成功”
分析及結論:
-
當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
2.解決辦法:
如果使用給資料庫加鎖的方式,在給處理多個商品時可能會出現死鎖,所以使用資料庫中的樂觀鎖方式來處理效果較好
資料庫樂觀鎖:
樂觀鎖並不是真實存在的鎖,而是在更新的時候判斷此時的庫存是否是之前查詢出的庫存,如果相同,表示沒人修改,可以更新庫存,否則表示別人搶過資源,不再執行庫存更新。類似如下操作
使用原生的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使用者返回 “商品庫存不足”
3.需要修改MySQL的事務隔離級別
事務隔離級別指的是在處理同一個資料的多個事務中,一個事務修改資料後,其他事務何時能看到修改後的結果。
MySQL資料庫事務隔離級別主要有四種:
Serializable 序列化,一個事務一個事務的執行
Repeatable read 可重複讀,無論其他事務是否修改並提交了資料,在這個事務中看到的資料值始終不受其他事務影響
Read committed 讀取已提交,其他事務提交了對資料的修改後,本事務就能讀取到修改後的資料值
Read uncommitted 讀取為提交,其他事務只要修改了資料,即使未提交,本事務也能看到修改後的資料值。
MySQL資料庫預設使用可重複讀( Repeatable read),而使用樂觀鎖的時候,如果一個事務修改了庫存並提交了事務,那其他的事務應該可以讀取到修改後的資料值,所以不能使用可重複讀的隔離級別,應該修改為讀取已提交Read committed。
修改方法:
-
開啟配置檔案
-
修改隔離級別