mssql-django 中的事务管理

本文介绍如何使用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 UNCOMMITTEDNOLOCK 都允许脏读,这意味着查询可以返回来自未提交事务的数据。 仅将这些技术用于不需要绝对一致性的报告或分析查询。

选项 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

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]
参数 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 的差异

  • 参数 ofselect_for_update(of=(...)))不受支持。 如果将其传给后端,后端会引发 NotSupportedError
  • SQL Server使用表级提示(UPDLOCK)而不是行级FOR UPDATE子句。 在高争用情况下,锁升级可能导致锁定的行或页比你预期的更多。 如果您需要在锁定写入的同时进行非阻塞读取,请使用 SNAPSHOT 隔离级别。