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)
我們先使用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。
修改方法:
-
打開配置文件

-
修改隔離級別

