mssql-django 中的交易管理

本文說明如何透過 SQL Server 後端設定 Django 應用程式mssql-django的交易處理與隔離層級。

預設行為

預設情況下,Django 採用自動提交模式。 每個資料庫查詢都以自己的交易執行,並立即提交。 你可以用設定 AUTOCOMMIT 或 Django 的交易管理 API 來更改這個行為。

AUTOCOMMIT 設定

在資料庫組態中將 AUTOCOMMIT 設為 False,以停用自動提交模式:

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "USER": "<your-username>",
        "PASSWORD": "<your-password>",
        "HOST": "<your-server>",
        "PORT": "1433",
        "AUTOCOMMIT": False,
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
        },
    },
}

Note

停用自動提交表示您必須明確地提交或回滾交易。 大多數 Django 應用程式都維持啟用自動提交,並在特定操作中使用 transaction.atomic()

請使用 transaction.atomic()

將資料庫操作包裝進 transaction.atomic() 去,確保它們能在單一交易中執行:

from django.db import transaction
from myapp.models import Account

def transfer_funds(from_account_id, to_account_id, amount):
    with transaction.atomic():
        sender = Account.objects.select_for_update().get(pk=from_account_id)
        receiver = Account.objects.select_for_update().get(pk=to_account_id)

        sender.balance -= amount
        receiver.balance += amount

        sender.save()
        receiver.save()

如果在 atomic() 區塊內發生任何例外,整個交易處理都會復原。

巢狀交易

Django 支援透過 SQL Server 的儲存點進行巢狀atomic()區塊:

from django.db import transaction

with transaction.atomic():
    # Outer transaction
    Product.objects.create(name="Widget A", price=9.99)

    try:
        with transaction.atomic():
            # Inner savepoint
            Product.objects.create(name="Widget B", price=14.99)
            raise ValueError("Simulated error")
    except ValueError:
        pass  # Inner savepoint is rolled back, outer continues

    # Widget A is committed, Widget B is not

交易隔離層級

請使用 isolation_level 資料庫設定中的選項來設定交易隔離層級:

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "USER": "<your-username>",
        "PASSWORD": "<your-password>",
        "HOST": "<your-server>",
        "PORT": "1433",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
            "isolation_level": "READ COMMITTED",
        },
    },
}

支援隔離等級

隔離等級 Description
READ UNCOMMITTED 允許髒評。 最低的隔離度,最高的並行。
READ COMMITTED SQL Server 預設值。 防止髒讀。
REPEATABLE READ 避免髒讀和無法重複讀取。
SNAPSHOT 使用列版本控制來確保讀取一致且不會阻塞。 需要啟用資料庫層級快照隔離。
SERIALIZABLE 最高隔離。 防止幻讀。

啟用 SNAPSHOT 隔離

要使用 SNAPSHOT 隔離,請先在資料庫啟用:

ALTER DATABASE [<your-database>]
SET ALLOW_SNAPSHOT_ISOLATION ON;

然後在 settings.py 中進行設定:

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "USER": "<your-username>",
        "PASSWORD": "<your-password>",
        "HOST": "<your-server>",
        "PORT": "1433",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
            "isolation_level": "SNAPSHOT",
        },
    },
}

請使用 @transaction.atomic 裝飾器

將交易套用至整個檢視函式:

from django.db import transaction
from django.http import JsonResponse

@transaction.atomic
def create_order(request):
    # All database operations in this view run in a single transaction
    order = Order.objects.create(customer_id=request.user.id)
    for item in request.POST.getlist("items"):
        OrderItem.objects.create(order=order, product_id=item)
    return JsonResponse({"order_id": order.pk})

無需阻塞即可讀取資料(NOLOCK 等效)

常見的要求是使用 NOLOCK 提示或 READ UNCOMMITTED 隔離層級來查詢 SQL Server,以避免在繁忙的資料表上發生封鎖。 Django 的 ORM 不會產生表格提示,但你有兩個選擇。

選項 1:針對每個連線設定 READ UNCOMMITTED(未提交讀取)

在專用的唯讀資料庫別名上,將隔離層級設為 READ UNCOMMITTED,以將其套用至該連線上的所有查詢:

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "HOST": "<your-server>",
        "PORT": "1433",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
        },
    },
    "read_uncommitted": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "HOST": "<your-server>",
        "PORT": "1433",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
            "isolation_level": "READ UNCOMMITTED",
        },
    },
}

接著將查詢路由到 read_uncommitted 別名:

# Read with NOLOCK-equivalent behavior
products = Product.objects.using("read_uncommitted").filter(active=True)

# Writes still go through the default connection
Product.objects.create(name="Widget", price=9.99)

選項二:使用 NOLOCK 的原始 SQL

針對特定資料表進行目標查詢,請使用帶有 NOLOCK 表格提示的原始 SQL 資料:

from django.db import connection

with connection.cursor() as cursor:
    cursor.execute("SELECT id, name, price FROM myapp_product WITH (NOLOCK) WHERE active = %s", [1])
    rows = cursor.fetchall()

謹慎

READ UNCOMMITTEDNOLOCK 兩者都允許髒讀,也就是說,查詢可能會傳回來自未提交交易的資料。 這些技術只用於不需要絕對一致性的報告或分析查詢。

選項三:改用 SNAPSHOT 隔離

SNAPSHOT 隔離提供一致的讀取,且不會阻塞且不會有髒讀。 對大多數工作負載而言,這是建議用來取代 NOLOCK 的方案:

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "HOST": "<your-server>",
        "PORT": "1433",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
            "isolation_level": "SNAPSHOT",
        },
    },
}

SNAPSHOT 需要資料庫層級的設定。 請參見 啟用 SNAPSHOT 隔離

使用 select_for_update() 進行資料列層級鎖定

Django 的 select_for_update() 可獲得 mssql-django 後端的完整支援。 SQL Server 採用表格提示(table hints)來實現此功能,而非FOR UPDATE其他資料庫使用的子句。

基本使用方式

from django.db import transaction

with transaction.atomic():
    product = Product.objects.select_for_update().get(pk=1)
    product.stock -= 1
    product.save()

後端會產生: SELECT ... FROM [myapp_product] WITH (ROWLOCK, UPDLOCK) WHERE ...

NOWAIT 並 SKIP 鎖定

nowaitskip_locked 這兩個參數皆受支援:

from django.db import transaction

# Raise DatabaseError immediately if the row is already locked
with transaction.atomic():
    product = Product.objects.select_for_update(nowait=True).get(pk=1)

# Skip rows that are locked by other transactions
with transaction.atomic():
    available = Product.objects.select_for_update(skip_locked=True).filter(
        reserved=False
    )[:10]
Parameter SQL Server 數據表提示
預設 WITH (ROWLOCK, UPDLOCK)
nowait=True WITH (NOWAIT, ROWLOCK, UPDLOCK)
skip_locked=True WITH (ROWLOCK, UPDLOCK, READPAST)

Note

select_for_update() 必須在 transaction.atomic() 區塊內使用。 如果你在交易外呼叫 Django,Django 會發出錯誤。

與 PostgreSQL 的差異

  • of參數(select_for_update(of=(...)))不被支援。 如果你將它傳給後端,後端會拋出 NotSupportedError
  • SQL Server 使用資料表層級的提示(UPDLOCK)而非列層FOR UPDATE級子句。 在高度爭用時,鎖定升級可能會導致實際鎖定的資料列或頁面數量超出您原本要鎖定的範圍。 如果你需要在鎖定寫入的同時進行非阻塞讀取,就使用 SNAPSHOT 隔離層級。