如何在Django中设置“可重复读”事务?

24
我有一个函数,对同一数据集进行多个查询,我希望确保所有查询看到的数据完全相同。
就SQL而言,这意味着对支持它的数据库使用REPEATABLE READ隔离级别。如果数据库不支持,我不介意使用更高的级别甚至完全锁定。
据我所见,情况并非如此。也就是说,如果我在一个Python shell中运行以下代码:
with transaction.atomic():
    for t in range(0, 60):
        print("{0}: {1}".format(t, MyModel.objects.count()))
        time.sleep(1)

只要我在另一个函数中执行MyModel.objects.create(...),正在运行的循环立即看到增加的值。这正是我想要避免的。进一步的测试表明,该行为与READ COMMITTED级别相匹配,这对我的口味来说过于宽松。
我还想强调一点,我只想为单个函数提供更严格的隔离级别,而不是整个项目。
我最好的选择是什么?
在我的特定情况下,我关心的唯一数据库是PostgreSQL 9.3+,但我也希望与SQLite3兼容,如果必要,甚至完全锁定整个数据库对我来说也可以。然而,显然,通用性更强的解决方案更受欢迎。

缓存这些元数据是可接受的方法吗? - Jossef Harush Kadouri
很遗憾,不行。我所执行的查询需要计算原始事件数据的各种统计信息,并且为了保持一致的视图,我必须将整个数据集加载到内存中,这是我真的不想做的事情。 - drdaeman
3个回答

23

你说得对,postgres中的默认事务隔离级别是READ COMMITTED。你可以在设置中轻松更改它以测试它是否适合你的需求:https://docs.djangoproject.com/en/1.8/ref/databases/#isolation-level

此外,我怀疑你不会面临一些性能问题,因为postgres在处理事务时非常高效,即使在SERIALIZABLE模式下也是如此。而mysql的默认隔离级别为REPEATABLE READ,我们看到它也不会影响性能。

无论如何,您都可以像这样手动设置隔离级别:

http://initd.org/psycopg/docs/extensions.html#isolation-level-constants

要设置自定义事务隔离级别,您可以尝试以下方法:

from django.db import connection

with transaction.atomic():
    cursor = connection.cursor()
    cursor.execute('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ')
    # logic

我建议您首先在设置中更改默认模式(如果可以的话)。

然后,如果符合您的需求,您可以将其删除并修改特定位置的代码。


感谢回复。我以前遇到过性能问题(我想这是大约五年前的事了),将默认隔离级别从SERIALIZABLE更改为READ COMMITTED(那是非Django项目)对性能有了合理的影响。也许这只是我的特殊情况,或者我碰到了一些错误。无论如何,我有点不愿意全局设置过于严格的隔离级别,并且真的很想知道是否有一种方法可以仅在单个函数或代码块的执行期间更改此设置。或者知道这是不可行的,因为ORM设计中的某些内容会阻止这样做。 - drdaeman
当然,有一些情况下严格的隔离模式可能过于严格了。不管怎样,PostgreSQL会尽力而为 :) 为代码的特定部分设置非默认的隔离级别是完全可以的。在整个项目中使用相同的隔离级别更加一致,因为它确保你至少不会忘记在某个地方这样做。已更新答案并附上了代码示例。 - alTus
3
问题在于您必须在任何查询之前调用 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ,而在Django中执行此操作并不简单。 - You knows who
2
你可以潜在地定义2个连接到同一个数据库,具有不同的隔离级别,并且在需要时使用更受限制的连接? - DanH
1
这里有一个答案,展示了如何设置两个不同隔离级别的独立连接,正如DanH所建议的。 - Christian Long
1
高隔离级别的主要问题在于你需要小心的不是隔离级别的开销(在Postgres上它们大多可以忽略不计),而是额外的阻塞(如果Postgres检测到可能的写冲突)和序列化异常,如果Postgres决定并发事务由于冲突写入已经提交而无法继续,则应用程序可能会出现这些异常。如果你的应用程序没有冲突,那么开销应该可以忽略不计。 - Lie Ryan

1

django-pgtransaction提供了django.db.transaction.atomic上下文管理器/装饰器的扩展,该扩展允许在打开事务时动态设置隔离级别,并指定当该事务中的操作导致Postgres锁定异常时的重试策略:

@pgtransaction.atomic(isolation_level=pgtransaction.SERIALIZABLE, retry=3)
def do_queries():
    # Do queries...

0

在Django中设置REPEATABLE READ有两种方法。

<第一种方法(我的推荐)>

您可以运行原始查询,在settings.py中的数据库设置之后设置隔离级别,如下所示。*如果在数据库设置之前运行原始查询,则会出现错误:

# "settings.py"

from django.db import connection

# ...

DATABASES = {
    'default':{
        'ENGINE':'django.db.backends.postgresql',
        'NAME':'postgres',
        'USER':'postgres',
        'PASSWORD':'admin',
        'HOST':'localhost',
        'PORT':'5432',
    },
}

# ↓ ↓ ↓ Set isolation level ↓ ↓ ↓

cursor = connection.cursor()
query = """
        ALTER DATABASE postgres 
        SET DEFAULT_TRANSACTION_ISOLATION 
        TO 'REPEATABLE READ';
        """
cursor.execute(query)

# ↓ ↓ ↓ Check isolation level ↓ ↓ ↓

cursor.execute('SHOW default_transaction_isolation;')
print(cursor.fetchone()) # ('repeatable read',)

每次使用以下命令运行Django服务器或通过编写代码重新加载Django服务器时,都会运行settings.py文件以设置事务:

python manage.py runserver 0.0.0.0:8000

<第二种方式>

您可以通过以下示例直接使用psql设置REPEATABLE READ

postgres=# ALTER DATABASE postgres SET DEFAULT_TRANSACTION_ISOLATION TO 'REPEATABLE READ';

实际上,文档下面展示的方法在我使用Django 3.2.16Windows 11上并不起作用。这就是为什么我展示了上面的两种方法:

# "settings.py"

import psycopg2.extensions

DATABASES = {
    'default':{
        'ENGINE':'django.db.backends.postgresql',
        'NAME':'postgres',
        'USER':'postgres',
        'PASSWORD':'admin',
        'HOST':'localhost',
        'PORT':'5432',
    },
    'OPTIONS': { # ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ Doesn't work ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
        'isolation_level': psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE,
    },
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接