1. 程式人生 > 實用技巧 >事務,悲觀鎖和樂觀鎖

事務,悲觀鎖和樂觀鎖

事務

事務(Transaction):一個最小的不可再分的工作單元;通常一個事務對應一個完整的業務(例如銀行賬戶轉賬業務,該業務就是一個最小的工作單元)

一個完整的業務需要批量的DML(insert、update、delete)語句共同聯合完成

事務只和DML語句 ( 資料庫操作語句 ) 有關,或者說DML語句才有事務。這個和業務邏輯有關,業務邏輯不同,DML語句的個數不同

特性

  • 原子性(A) 事務是最小單位,不可再分

  • 一致性(C) 事務要求所有的DML語句操作的時候,必須保證同時成功或者同時失敗

  • 隔離性(I) 事務A和事務B之間具有隔離性

  • 永續性(D) 是事務的保證,事務終結的標誌(記憶體的資料持久到硬碟檔案中)

行為

開啟事務 Start Transaction

事務結束 End Transaction

提交事務 Commit Transaction

回滾事務 Rollback Transaction

django開啟事務

from django.db import transaction

# 方式一
with transaction.atomic(): # 開啟事務,當with語句執行完成以後,自動提交事務
    ... # 資料庫操作



# 方式二
@transaction.atomic
def foo():
    ....
    sid=transaction.savepoint()  #開啟事務
    ...
    transaction.savepoint_rollback(sid)  # 回滾
    ...
    transaction.savepoint_commit(sid)  # 提交


# 事務回滾
from django.db import transaction
from rest_framework.views import APIView
class OrderAPIView(APIView):
    def post(self,request):
        ....
        with transation.atomic():
            # 設定事務回滾的標記點
            sid = transation.savepoint()

            ....

            try:
                ....
            except:
                transation.savepoint_rallback(sid)

悲觀鎖

總是假設最壞的情況,每次取資料時都認為其他執行緒會修改,所以都會加鎖(讀鎖、寫鎖、行鎖等)
當其他執行緒想要訪問資料時,都需要阻塞掛起。可以依靠資料庫實現,如行鎖、讀鎖和寫鎖等,都是在操作之前加鎖
保證同一時刻只有一個執行緒能操作資料,其他執行緒則會被 block

運用場景

▧ 無髒讀  上鎖資料保證一致, 因此無髒讀, 對髒讀不允許的環境悲觀鎖可以勝任 

▧ 無並行  悲觀鎖對事務成功性可以保證, 但是會對資料加鎖導致無法實現資料的並行處理.

▧ 事務成功率高  上鎖保證一次成功, 因此在對資料處理的成功率要求較高的時候更適合悲觀鎖.

▧ 開銷大  悲觀鎖的上鎖解鎖是有開銷的, 如果超大的併發量這個開銷就不容小視, 因此不適合在高併發環境中使用悲觀鎖 

▧ 一次性完成  如果樂觀鎖多次嘗試的代價比較大,也建議使用悲觀鎖, 悲觀鎖保證一次成功

使用

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')

樂觀鎖

總是認為不會產生併發問題,每次去取資料的時候總認為不會有其他執行緒對資料進行修改,因此不會上鎖
但是在更新時會判斷其他執行緒在這之前有沒有對資料進行修改,一般會使用版本號機制或CAS操作實現。
如果發現數據被改了. 就進行事務回滾取消之前的操作

運用場景

▧ 髒讀  樂觀鎖不涉及到上鎖的處理, 因此在資料並行需求的時候是更適合樂觀鎖,當然會產生髒讀, 不過用回滾取消掉了.

▧ 高併發  相比起悲觀鎖的開銷, 樂觀鎖也是比悲觀鎖更適合於高併發場景

▧ 事務成功率低  樂觀鎖不能保證每次事務的成功, 是使用回滾方式來保證資料一致性, 因此會導致事務成功率很低.

▧ 讀多寫少  樂觀鎖適用於讀多寫少的應用場景,這樣可以提高併發粒度

▧ 開銷小  可能會導致很多次的回滾都不能拿到正確的處理迴應, 因此如果對成功性要求低,而且每次開銷小比較適合樂觀鎖

使用

from django.shortcuts import render
from django.http import JsonResponse
from django.views.generic import View
from django.db import transaction
from 應用名.models import GoodsSKU
 
 
# 類檢視 (併發,樂觀鎖)
class MyView(View):
    
    @transaction.atomic
    def post(self, request):
        '''訂單建立'''
        count = 3   # 訂購3件商品
        
        # 設定事務儲存點
        s1 = transaction.savepoint()
        
        # 樂觀鎖,最多嘗試5次
        for i in range(5):
            # 查詢商品的資訊(庫存)
            try:
                sku = GoodsSKU.objects.get(id=1)
            except:
                # 商品不存在
                transaction.savepoint_rollback(s1)
                return JsonResponse({'res': 1, 'errmsg': '商品不存在'})
 
            # 判斷商品的庫存
            if count > sku.stock:
                transaction.savepoint_rollback(s1)
                return JsonResponse({'res': 2, 'errmsg': '商品庫存不足'})
 
            # 更新商品的庫存和銷量
            orgin_stock = sku.stock   # 原庫存 (資料庫隔離級別必須是Read Committed;如果是Repeatable Read,那麼多次嘗試讀取的原庫存都是一樣的,讀不到其他執行緒提交更新後的資料。)
            new_stock = orgin_stock - count   # 更新後的庫存
            new_sales = sku.sales + count   # 更新後的銷量
 
            # update 商品表 set stock=new_stock, sales=new_sales where id=1 and stock = orgin_stock
            # 通過where子句中的條件判斷庫存是否進行了修改。(併發,樂觀鎖)
            # 返回受影響的行數
            res = GoodsSKU.objects.filter(id=1, stock=orgin_stock).update(stock=new_stock, sales=new_sales)
            if res == 0:  # 如果修改失敗
                if i == 4:
                    # 如果嘗試5次都失敗
                    transaction.savepoint_rollback(s1)
                    return JsonResponse({'res': 3, 'errmsg': '下單失敗'})
                continue  # 再次嘗試
 
            # 否則更新成功
            # 跳出嘗試迴圈
            break
 
 
        # 提交事務
        transaction.savepoint_commit(s1)
 
        # 返回應答
        return JsonResponse({'res': 4, 'message': '建立成功'})