本文介绍如何使用mssql-django后端和 SQL Server 为 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 等效项)
一种常见的做法是,在查询 SQL Server 时使用 NOLOCK 提示或 READ UNCOMMITTED 隔离级别,以避免在繁忙的表上发生阻塞。 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)
选项 2:将原始 SQL 与 NOLOCK 配合使用
对于特定表的目标查询,请使用原始 SQL 和 NOLOCK 表提示:
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 都允许脏读,这意味着查询可以返回来自未提交事务的数据。 仅将这些技术用于不需要绝对一致性的报告或分析查询。
选项 3:改用 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 使用表提示来实现这一点,而不是像其他数据库那样使用 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 LOCKED
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]
| 参数 | 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,则引发错误。
与 PostgreSQL 的差异
- 参数
of(select_for_update(of=(...)))不受支持。 如果将其传给后端,后端会引发NotSupportedError。 - SQL Server使用表级提示(
UPDLOCK)而不是行级FOR UPDATE子句。 在高争用情况下,锁升级可能导致锁定的行或页比你预期的更多。 如果您需要在锁定写入的同时进行非阻塞读取,请使用SNAPSHOT隔离级别。