1. 程式人生 > >python資料庫併發處理(樂觀鎖)

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來模擬單個使用者請求

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配置檔案

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