Django之數據庫併發處理

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)

4.定義路由:

from django.conf.urls import url
from . import views

urlpatterns =[
    url(r'^goods/$', views.Goods.as_view()),
]

我們先使用postman來模擬單個用戶請求

postman

  • 再來查詢數據庫,單個用戶請求正常,(將stock恢復到10)
    查詢數據庫

模擬多個用戶請求

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

運行結果:

  • 輸出日誌:
    log
  • 查詢數據庫:
    查詢數據庫
  • 兩個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)

多用戶請求結果:

  • 輸出日誌:
    log_cuccess
  • 查詢數據庫
    查詢數據庫
  • A用戶返回 “操作成功”, B用戶返回 “商品庫存不足”

3.需要修改MySQL的事務隔離級別

事務隔離級別指的是在處理同一個數據的多個事務中,一個事務修改數據後,其他事務何時能看到修改後的結果。

MySQL數據庫事務隔離級別主要有四種:

Serializable 串行化,一個事務一個事務的執行

Repeatable read 可重複讀,無論其他事務是否修改並提交了數據,在這個事務中看到的數據值始終不受其他事務影響

Read committed 讀取已提交,其他事務提交了對數據的修改後,本事務就能讀取到修改後的數據值

Read uncommitted 讀取爲提交,其他事務只要修改了數據,即使未提交,本事務也能看到修改後的數據值。

MySQL數據庫默認使用可重複讀( Repeatable read),而使用樂觀鎖的時候,如果一個事務修改了庫存並提交了事務,那其他的事務應該可以讀取到修改後的數據值,所以不能使用可重複讀的隔離級別,應該修改爲讀取已提交Read committed。

修改方法:

  • 打開配置文件
    打開mysql配置文件

  • 修改隔離級別
    修改隔離級別

OK

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章