本文說明如何透過 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 UNCOMMITTED 和 NOLOCK 兩者都允許髒讀,也就是說,查詢可能會傳回來自未提交交易的資料。 這些技術只用於不需要絕對一致性的報告或分析查詢。
選項三:改用 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 鎖定
nowait 和 skip_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隔離層級。