1. 程式人生 > >08.Django基礎六之ORM中的鎖和事務

08.Django基礎六之ORM中的鎖和事務

一 鎖

  行級鎖

    select_for_update(nowait=False, skip_locked=False) #注意必須用在事務裡面,至於如何開啟事務,我們看下面的事務一節。

    返回一個鎖住行直到事務結束的查詢集,如果資料庫支援,它將生成一個 SELECT ... FOR UPDATE 語句。

    舉個例子:

entries = Entry.objects.select_for_update().filter(author=request.user)  #加互斥鎖,由於mysql在查詢時自動加的是共享鎖,所以我們可以手動加上互斥鎖。create、update、delete操作時,mysql自動加行級互斥鎖

    所有匹配的行將被鎖定,直到事務結束。這意味著可以通過鎖防止資料被其它事務修改。

    一般情況下如果其他事務鎖定了相關行,那麼本查詢將被阻塞,直到鎖被釋放。 如果這不想要使查詢阻塞的話,使用select_for_update(nowait=True)。 如果其它事務持有衝突的鎖,互斥鎖, 那麼查詢將引發 DatabaseError 異常。你也可以使用select_for_update(skip_locked=True)忽略鎖定的行。 nowait和  skip_locked是互斥的,同時設定會導致ValueError。

    目前,postgresql,oracle和mysql資料庫後端支援select_for_update()。 但是,MySQL不支援nowait和skip_locked引數。

    使用不支援這些選項的資料庫後端(如MySQL)將nowait=True或skip_locked=True轉換為select_for_update()將導致丟擲DatabaseError異常,這可以防止程式碼意外終止。

  表鎖(瞭解)

class LockingManager(models.Manager):
    """ Add lock/unlock functionality to manager.

    Example::

        class Job(models.Model): #其實不用這麼負載,直接在orm建立表的時候,給這個表定義一個lock和unlock方法,藉助django提供的connection模組來發送鎖表的原生sql語句和解鎖的原生sql語句就可以了,不用外層的這個LckingManager(model.Manager)類

            manager = LockingManager()

            counter = models.IntegerField(null=True, default=0)

            @staticmethod
            def do_atomic_update(job_id)
                ''' Updates job integer, keeping it below 5 '''
                try:
                    # Ensure only one HTTP request can do this update at once.
                    Job.objects.lock()

                    job = Job.object.get(id=job_id)
                    # If we don't lock the tables two simultanous
                    # requests might both increase the counter
                    # going over 5
                    if job.counter < 5:
                        job.counter += 1                                        
                        job.save()

                finally:
                    Job.objects.unlock()


    """    

    def lock(self):
        """ Lock table. 

        Locks the object model table so that atomic update is possible.
        Simulatenous database access request pend until the lock is unlock()'ed.

        Note: If you need to lock multiple tables, you need to do lock them
        all in one SQL clause and this function is not enough. To avoid
        dead lock, all tables must be locked in the same order.

        See http://dev.mysql.com/doc/refman/5.0/en/lock-tables.html
        """
        cursor = connection.cursor()
        table = self.model._meta.db_table
        logger.debug("Locking table %s" % table)
        cursor.execute("LOCK TABLES %s WRITE" % table)
        row = cursor.fetchone()
        return row

    def unlock(self):
        """ Unlock the table. """
        cursor = connection.cursor()
        table = self.model._meta.db_table
        cursor.execute("UNLOCK TABLES")
        row = cursor.fetchone()
        return row  

二 事務

  

  關於MySQL的事務處理,我的mysql部落格已經說的很清楚了,那麼我們來看看Django是如果做事務處理的。django1.8版本之前是有很多種新增事務的方式的,中介軟體的形式(全域性的)、函式裝飾器的形式,上下文管理器的形式等,但是很多方法都在1.8版之後給更新了,下面我們只說最新的:

1 全域性開啟

    在Web應用中,常用的事務處理方式是將每個請求都包裹在一個事務中。這個功能使用起來非常簡單,你只需要將它的配置項ATOMIC_REQUESTS設定為True。

    它是這樣工作的:當有請求過來時,Django會在呼叫檢視方法前開啟一個事務。如果請求卻正確處理並正確返回了結果,Django就會提交該事務。否則,Django會回滾該事務。

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'mxshop',
        'HOST': '127.0.0.1',
        'PORT': '3306',
        'USER': 'root',
        'PASSWORD': '123',
        'OPTIONS': {
            "init_command": "SET default_storage_engine='INNODB'",
       #'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", #配置開啟嚴格sql模式


        }
        "ATOMIC_REQUESTS": True, #全域性開啟事務,繫結的是http請求響應整個過程        "AUTOCOMMIT":False, #全域性取消自動提交,慎用
    },  'other':{    'ENGINE': 'django.db.backends.mysql',             ......  } #還可以配置其他資料庫
}

    上面這種方式是統一個http請求對應的所有sql都放在一個事務中執行(要麼所有都成功,要麼所有都失敗)。是全域性性的配置, 如果要對某個http請求放水(然後自定義事務),可以用non_atomic_requests修飾器,那麼他就不受事務的管控了

from django.db import transaction

@transaction.non_atomic_requests
def my_view(request):
    do_stuff()

@transaction.non_atomic_requests(using='other')
def my_other_view(request):
    do_stuff_on_the_other_database()

    但是Django 文件中說,不推薦這麼做。因為如果將事務跟 HTTP 請求繫結到一起的時,然而view 是依賴於應用程式對資料庫的查詢語句效率和資料庫當前的鎖競爭情況。當流量上來的時候,效能會有影響,知道一下就行了

    所以推薦用下面這種方式,通過 transaction.atomic 來更加明確的控制事務。atomic允許我們在執行程式碼塊時,在資料庫層面提供原子性保證。 如果程式碼塊成功完成, 相應的變化會被提交到資料庫進行commit;如果執行期間遇到異常,則會將該段程式碼所涉及的所有更改回滾。

2 區域性使用事務

    atomic(using=None, savepoint=True)[source] ,引數:using='other',就是當你操作其他資料庫的時候,這個事務才生效,看上面我們的資料庫配置,除了default,還有一個other,預設的是default。savepoint的意思是開啟事務儲存點,推薦看一下我資料庫部落格裡面的事務部分關於儲存點的解釋。

原子性是資料庫事務的一個屬性。使用atomic,我們就可以建立一個具備原子性的程式碼塊。一旦程式碼塊正常執行完畢,所有的修改會被提交到資料庫。反之,如果有異常,更改會被回滾。

    被atomic管理起來的程式碼塊還可以內嵌到方法中。這樣的話,即便內部程式碼塊正常執行,如果外部程式碼塊丟擲異常的話,它也沒有辦法把它的修改提交到資料庫中。

    用法1:給函式做裝飾器來使用 

from django.db import transaction

@transaction.atomic
def viewfunc(request):
    # This code executes inside a transaction.
    do_stuff()

    用法2:作為上下文管理器來使用,其實就是設定事務的儲存點

from django.db import transaction

def viewfunc(request):
    # This code executes in autocommit mode (Django's default).
    do_stuff()

    with transaction.atomic():   #儲存點
        # This code executes inside a transaction.
        do_more_stuff()

    do_other_stuff()

      一旦把atomic程式碼塊放到try/except中,完整性錯誤就會被自然的處理掉了,比如下面這個例子:

from django.db import IntegrityError, transaction

@transaction.atomic
def viewfunc(request):
    create_parent()

    try:
        with transaction.atomic():
            generate_relationships()
    except IntegrityError:
        handle_exception()

    add_children()

    用法3:還可以巢狀使用,函式的事務巢狀上下文管理器的事務,上下文管理器的事務巢狀上下文管理器的事務等。下面的是函式巢狀上下文的例子:

from django.db import IntegrityError, transaction

@transaction.atomic
def viewfunc(request):
    create_parent()

    try:
        with transaction.atomic():
            generate_relationships()       #other_task()  #還要注意一點,如果你在事務裡面寫了別的操作,只有這些操作全部完成之後,事務才會commit,也就是說,如果你這個任務是查詢上面更改的資料表裡面的資料,那麼看到的還是事務提交之前的資料。
    except IntegrityError:
        handle_exception()

    add_children()

      這個例子中,即使generate_relationships()中的程式碼打破了資料完整性約束,你仍然可以在add_children()中執行資料庫操作,並且create_parent()產生的更改也有效。需要注意的是,在呼叫handle_exception()之前,generate_relationships()中的修改就已經被安全的回滾了。因此,如果有需要,你照樣可以在異常處理函式中操作資料庫。

儘量不要在atomic程式碼塊中捕獲異常

  因為當atomic塊中的程式碼執行完的時候,Django會根據程式碼正常執行來執行相應的提交或者回滾操作。如果在atomic程式碼塊裡面捕捉並處理了異常,就有可能隱蓋程式碼本身的錯誤,從而可能會有一些意料之外的不愉快事情發生。

  擔心主要集中在DatabaseError和它的子類(如IntegrityError)。如果這種異常真的發生了,事務就會被破壞掉,而Django會在程式碼執行完後執行回滾操作。如果你試圖在回滾前執行一些資料庫操作,Django會丟擲TransactionManagementError。通常你會在一個ORM相關的訊號處理器丟擲異常時遇到這個行為。

捕獲異常的正確方式正如上面atomic程式碼塊所示。如果有必要,新增額外的atomic程式碼塊來做這件事情,也就是事務巢狀。這麼做的好處是:當異常發生時,它能明確地告訴你那些操作需要回滾,而那些是不需要的。

    為了保證原子性,atomic還禁止了一些API。像試圖提交、回滾事務,以及改變資料庫連線的自動提交狀態這些操作,在atomic程式碼塊中都是不予許的,否則就會丟擲異常。

  下面是Django的事務管理程式碼:

  • 進入最外層atomic程式碼塊時開啟一個事務;
  • 進入內部atomic程式碼塊時建立儲存點;
  • 退出內部atomic時釋放或回滾事務;注意如果有巢狀,內層的事務也是不會提交的,可以釋放(正常結束)或者回滾
  • 退出最外層atomic程式碼塊時提交或者回滾事務;

    你可以將儲存點引數設定成False來禁止內部程式碼塊建立儲存點。如果發生了異常,Django在退出第一個父塊的時候執行回滾,如果存在儲存點,將回滾到這個儲存點的位置,否則就是回滾到最外層的程式碼塊。外層事務仍然能夠保證原子性。然而,這個選項應該僅僅用於儲存點開銷較大的時候。畢竟它有個缺點:會破壞上文描述的錯誤處理機制。

  注意:transaction只對資料庫層的操作進行事務管理,不能理解為python操作的事務管理

def example_view(request):
    tag = False
    with transaction.atomic():
        tag = True
        change_obj() # 修改物件變數
        obj.save()
        raise DataError
    print("tag = ",tag) #結果是True,也就是說在事務中的python變數賦值,即便是事務回滾了,這個賦值也是成功的

  還要注意:如果你配置了全域性的事務,它和區域性事務可能會產生衝突,你可能會發現你區域性的事務完成之後,如果你的函式裡面其他的sql除了問題,也就是沒在這個上下文管理器的區域性事務包裹範圍內的函式裡面的其他的sql出現了問題,你的區域性事務也是提交不上的,因為全域性會回滾這個請求和響應所涉及到的所有的sql,所以還是建議以後的專案儘量不要配置全域性的事務,通過區域性事務來搞定,當然了,看你們的業務場景。

transaction的其他方法

@transaction.atomic
def viewfunc(request):

  a.save()
  # open transaction now contains a.save()
  sid = transaction.savepoint()  #建立儲存點

  b.save()
  # open transaction now contains a.save() and b.save()

  if want_to_keep_b:
      transaction.savepoint_commit(sid) #提交儲存點
      # open transaction still contains a.save() and b.save()
  else:
      transaction.savepoint_rollback(sid)  #回滾儲存點
      # open transaction now contains only a.save()

  transaction.commit() #手動提交事務,預設是自動提交的,也就是說如果你沒有設定取消自動提交,那麼這句話不用寫,如果你配置了那個AUTOCOMMIT=False,那麼就需要自己手動進行提交。

  為保證事務的隔離性,我們還可以結合上面的鎖來實現,也就是說在事務裡面的查詢語句,咱們使用select_for_update顯示的加鎖方式來保證隔離性,事務結束後才會釋放這個鎖,例如:(瞭解)

@transaction.atomic ## 輕鬆開啟事務
def handle(self):
    ## 測試是否存在此使用者
    try:
        ## 鎖定被查詢行直到事務結束
        user = 
    User.objects.select_for_update().get(open_id=self.user.open_id)
        #other sql 語句
    except User.DoesNotExist:
        raise BaseError(-1, 'User does not exist.')
    

  通過Django外部的python指令碼來測試一下事務:

import os

if __name__ == '__main__':
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "BMS.settings")
    import django
    django.setup()

    import datetime
    from app01 import models

    try:
        from django.db import transaction
        with transaction.atomic():
            new_publisher = models.Publisher.objects.create(name="火星出版社")
            models.Book.objects.create(title="橘子物語", publish_date=datetime.date.today(), publisher_id=10)  # 指定一個不存在的出版社id
    except Exception as e:
        print(str(e))

  下面再說一些設定事務的小原則吧:

    1.保持事務短小
    2.儘量避免事務中rollback
    3.儘量避免savepoint
    4.預設情況下,依賴於悲觀鎖
    5.為吞吐量要求苛刻的事務考慮樂觀鎖
    6.顯示宣告開啟事務
    7.鎖的行越少越好,鎖的時間越短越