【问题标题】:Does Django Atomic Transaction lock the database?Django Atomic Transaction 是否锁定数据库?
【发布时间】:2017-07-20 03:24:45
【问题描述】:

当你这样做时:

@transaction.atomic
def update_db():
    do_bulk_update()

当函数运行时,它会锁定数据库吗?

我在询问 django 的原子事务: https://docs.djangoproject.com/en/1.10/topics/db/transactions/#autocommit-details

【问题讨论】:

标签: django database locking atomic


【解决方案1】:

(我在这个答案中假设现代 SQL 数据库。)

tl;博士

事务不是锁,而是持有在操作过程中自动获取的锁。而django默认不加任何锁,所以答案是否定的,它不加锁数据库。

例如如果你这样做了:

@transaction.atomic
def update_db():
    cursor.execute('UPDATE app_model SET model_name TO 'bob' WHERE model_id = 1;')
    # some other stuff...

在“其他内容”期间,您将锁定 ID 为 1 的 app_model 行。但直到该查询它才被锁定。因此,如果您想确保一致性,您可能应该明确使用锁。

交易

如前所述,事务不是锁,因为这会影响性能。一般来说,它们首先是轻量级的机制,用于确保如果您进行大量更改,这些更改一次对数据库的其他用户没有意义,这些更改似乎会同时发生。 IE。是原子的。事务不会阻止其他用户更改数据库,实际上通常不会阻止其他用户更改您可能正在读取的相同行。

有关如何保护事务的更多详细信息,请参阅 this guide 和您的数据库文档(例如 postgres)。

原子的Django实现。

当您使用atomic 装饰器时,Django 本身会执行以下操作(参考the code)。

尚未在原子块中

  1. 禁用自动提交。自动提交是一个应用程序级别的功能,它总是会立即提交事务,所以它看起来就像没有一个未完成的事务一样。

    这告诉数据库开始一个新事务。

    • 此时psycopg2 for postgres 将事务的隔离级别设置为READ COMMITTED,这意味着事务中的任何读取都只会返回已提交的数据,这意味着如果另一个事务写入,您将看不到更改直到它提交。这确实意味着,如果该事务在您的事务期间提交,您可能会再次阅读并看到该值在您的事务期间发生了变化。

      显然这意味着数据库没有被锁定。

  2. 运行您的代码。您所做的任何查询/突变都不会提交。

  3. 提交事务。

  4. 重新启用自动提交。

在较早的原子块中

基本上在这种情况下,我们尝试使用保存点,以便在“回滚”“事务”时可以恢复到它们,但就数据库连接而言,我们处于同一个事务中。

自动锁定

如前所述,数据库可能会为您的事务提供一些自动锁定,如this doc 中所述。为了证明这一点,请考虑以下代码,它在一个包含一个表和一个行的 postgres 数据库上运行:

my_table
id | age
---+----
1  | 50

然后你运行这段代码:

import psycopg2 as Database
from multiprocessing import Process
from time import sleep
from contextlib import contextmanager


@contextmanager
def connection():
    conn = Database.connect(
        user='daphtdazz', host='localhost', port=5432, database='db_test'
    )
    try:
        yield conn
    finally:
        conn.close()

def connect_and_mutate_after_seconds(seconds, age):

    with connection() as conn:
        curs = conn.cursor()
        print('execute update age to %d...' % (age,))
        curs.execute('update my_table set age = %d where id = 1;' % (age,))
        print('sleep after update age to %d...' % (age,))
        sleep(seconds)
        print('commit update age to %d...' % (age,))
        conn.commit()


def dump_table():
    with connection() as conn:
        curs = conn.cursor()
        curs.execute('select * from my_table;')
        print('table: %s' % (curs.fetchall(),))

if __name__ == '__main__':

    p1 = Process(target=connect_and_mutate_after_seconds, args=(2, 99))
    p1.start()

    sleep(0.6)
    p2 = Process(target=connect_and_mutate_after_seconds, args=(1, 100))
    p2.start()
    p2.join()

    dump_table()

    p1.join()

    dump_table()

你得到:

execute update age to 99...
sleep after update age to 99...
execute update age to 100...
commit update age to 99...
sleep after update age to 100...
commit update age to 100...
table: [(1, 100)]
table: [(1, 100)]

关键是第二个进程在第一个命令完成之前启动,但是在它调用了update 命令之后,所以第二个进程必须等待锁定,这就是为什么我们看不到@987654335 @ 直到 99 岁的 commit 之后。

如果你把 sleep 放在 exec 之前,你会得到:

sleep before update age to 99...
sleep before update age to 100...
execute update age to 100...
commit update age to 100...
table: [(24, 3), (100, 2)]
execute update age to 99...
commit update age to 99...
table: [(24, 3), (99, 2)]

指示在第二个进程进行更新时未获得锁,更新发生在第一个进程的事务期间。

【讨论】:

  • 谢谢。因此,它不会锁定,但会保持代码块中数据库事务的完整性。结果可能会有所不同,具体取决于首先启动(和完成)块的进程。
【解决方案2】:

正如@daphtdazz answer所述,Django 在您打开事务时不会获取任何锁,但是当您更新数据时,数据库可能会获取自动锁。锁的类型和范围取决于数据库,也可能取决于事务隔离级别。有关这些自动锁定的详细信息,请参阅您的数据库文档。

如果您想手动上锁,有几个选项。

主要和最简单的一个是执行select_for_update() 查询。这将获取一个更新锁,该锁将阻止对与查询匹配的行的所有其他更新。这与更新事务中的行时自动获取的锁相同,但select_for_update() 允许您在实际进行更新之前获取更新锁,这通常很有用。

如果行锁不适合您的情况,您可以在支持它们的数据库(例如 Postgres)中获取建议锁。开箱即用,Django 不支持这一点,但有第三方包为 Django 添加了对咨询锁的支持,或者您可以简单地发出适当的原始 SQL 查询。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-07-30
  • 2015-05-11
  • 1970-01-01
  • 2020-01-04
  • 2015-04-26
  • 2010-10-29
  • 2022-11-11
相关资源
最近更新 更多