【问题标题】:Weird behaviour for transactions in django 1.6.1django 1.6.1 中事务的奇怪行为
【发布时间】:2014-03-30 00:23:42
【问题描述】:

我在 django 1.6 中使用 transaction.atomic 作为事务的上下文管理器。有一个代码块,我想在一个事务中,它有几个网络调用和一些数据库写入。我看到非常奇怪的行为。每隔一段时间(可能是 20 次中的 1 次),我注意到发生了部分回滚,没有引发任何异常,并且视图执行时没有任何错误。我的应用程序托管在 heroku 上,我们使用 heroku postgres v9.2.8。伪代码:

from django.db import transaction

def some_view(request):

    try:
        with transation.atomic():
            network_call_1()
            db_write_1.save(update_fields=['col4',])
            db_write_2.save(update_fields=['col3',])
            db_write_3.save(update_fields=['col1',])
            network_call_2()
            db_write_4.save(update_fields=['col6',])
            db_write_5.bulk_create([object1, object2])
            db_write_6.bulk_create([object1, object2])
    except Exception, e:
        logger.error(e)

    return HttpResponse()

我注意到的行为是,没有任何异常被引发,要么 db write 1-3 已回滚,其余部分已完成,要么 db write 1 已回滚,其余部分已完成,依此类推。我不明白为什么会发生这种情况。首先,如果有回滚,不应该是事务的完整回滚吗?如果有回滚,是否也不应该引发异常以便我知道发生了回滚?每次发生这种情况时,都不会引发异常,代码只是继续执行并返回成功的 HttpResponse。

相关设置:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'mydb',
        'USER': 'root',
        'PASSWORD': 'root',
        'HOST': 'localhost',
        'PORT': '5432',
    },
}
CONN_MAX_AGE = None

这个错误让我很困惑。任何线索都会有很大帮助!

【问题讨论】:

  • 我删除了我的答案,因为@almalki 证明我对文档的理解是错误的。但是,我认为这里的主要问题是您没有遇到异常,因此您不知道出了什么问题。为此,您模棱两可的 try 块可能正在吞噬一个内部错误似乎仍然是合理的(至少对我而言)。你能至少试试我的建议,并告诉我这是否重要?
  • 另外——你可能在使用autocommit=False吗?
  • Django 1.6 默认使用 autocommit=True。我还没有将其设置为 False。除非我对默认行为有误,否则我认为就自动提交而言我很好。尝试在事务块之外。即使错误被默默吞噬,整个事务也应该回滚。除非明确执行,否则不会发生部分回滚。这就是我对此感到困惑的主要原因。
  • 是的,自动提交默认为 True。我之所以问,是因为 django 1.6.2 引入了针对 autcommit=False 问题的修复程序...好吧,无论如何,我不知道是什么问题,但也许可以尝试将 autocommit=True 添加到您的数据库配置 (here's how) 所以它将在数据库级别强制执行。也许这会有所帮助
  • 重现性如何?我会通过设置 PostgreSQL log_statements='all' 来解决这个问题,这样我就可以准确地看到向 PostgreSQL 发出的命令序列是什么。

标签: django postgresql heroku transactions


【解决方案1】:

经过数小时的调试,我们找到了罪魁祸首。

当我们在 gunicorn 上启动应用程序时,它会产生工人。到达同一个 worker 的每个请求都使用同一个 django DatabaseWrapper 实例(在我们的例子中是 postgres),也称为连接。如果在一个请求的事务处理过程中,工作人员要接收另一个请求,则此请求会重置连接状态,从而导致事务以意外方式运行,如以下错误所述:https://code.djangoproject.com/ticket/21239 有时事务没有被提交,并且没有引发异常让您知道发生了。有时它的一部分确实会被提交,而其余部分会丢失,看起来像是部分回滚。

我们认为一个连接是线程安全的,但这里的这个 gunicorn 修补魔法确保不是这种情况:https://github.com/benoitc/gunicorn/blob/18.0/gunicorn/management/commands/run_gunicorn.py#L16

如果可能的话,仍然愿意接受有关如何回避这个问题的建议。

编辑:不要使用 run_gunicorn 管理命令来启动 Django。它做了一些时髦的补丁,导致数据库连接不是线程安全的。对我们有用的解决方案是只使用“gunicorn myapp.wsgi:application -c gunicorn.conf”。 Django 持久性数据库连接不适用于 gevent worker 类型,因此除非您想用完连接,否则请避免使用它。

【讨论】:

  • 我仍然认为我上面的回答是有效的。你必须在 except 末尾添加“raise”。
  • @matija 如果在事务块中捕获异常,您的回答是有意义的。即使这样,如果您愿意,也可以捕获非 db 异常。事务块中的任何数据库异常都会终止事务。不过,在这种情况下不会有任何部分提交/回滚。
  • 我检查了“原子”的代码。似乎只有 DatabaseError 进行回滚。所以我的回答是无效的,但它仍然可能是你麻烦的原因。
【解决方案2】:

我的 3 美分:

例外情况

我们确定没有发生异常。但我们是吗?您的伪代码仅通过记录来“处理”异常。确保loggingpass 在其他地方没有“处理”异常。

部分回滚

我们希望回滚整个事务,而不仅仅是部分。由于 django 1.6 nested 原子事务创建了一个 savepoint 并且回滚返回到最后一个保存点。确保没有嵌套事务。也许您有transaction middleware 主动检查ATOMIC_REQUESTSMIDDLEWARE_CLASSES。也许事务是在那些network_call 函数中开始的。

复制

因为network_call 代码可能会阻塞。尝试用模拟调用替换它们,即超时(可能不在生产中)。如果这导致 100%(部分)回滚。它应该更容易定位部分回滚的问题。

【讨论】:

    【解决方案3】:

    我先说几句。

    这段代码不必有异常,仍然有回滚。

    也许在这段代码之外存在某种超时。想想你是否在第二次网络调用中间杀死了 python 进程。不会记录此特定异常。

    我也建议添加

    raise

    在异常结束时,它将记录并重新引发相同的异常。捕获所有异常很少是好的。

    另外,可能存在线程问题。尝试在记录器中导入 threding 并记录当前线程 ID,但有例外。你可能会发现你实际上有多个线程,所以一个必须等​​待另一个。

    一般来说,在事务处理过程中进行一些外部调用并不是一个好主意。

    在开始原子事务之前执行两个调用,这样可以尽可能快。

    希望这会有所帮助。

    【讨论】:

      【解决方案4】:

      不是 Django 专家,但我知道 Postgres。我同意您的评估,即这听起来像是非常不典型的事务行为:回滚应该是全有或全无,并且应该有一个例外。既然如此,你能绝对确定这是回滚式的情况吗?还有许多其他可能的原因可能导致数据库中出现的数据与您预期的不同,其中许多场景更适合您观察到的回滚事件。

      您没有提供有关您的数据的任何细节,但我想象的是,您会看到类似“我将 col4 的值设置为 'foo',但在提交之后,旧值 'bar'仍在数据库中。”那是对的吗?

      如果是这样,那么其他可能的原因可能是:

      • 应该以某种方式设置“foo”值的代码有时实际上设置了现有的“bar”值或 NULL 值。
      • 代码正在设置 'foo' 值,但是有一个带有 'dirty' 标志的数据访问层(又名 DAL)没有被设置(例如,如果对象处于断开状态),所以当提交完成后,DAL 并不认为这是它应该写入的更改。

      这些只是帮助您入门的几个示例。还有很多其他可能的情况。有时,调试此类问题的基本原理类似于 DDT 和 pelicans 的问题:由于数据库位于食物链的顶端,因此您经常会在那里看到问题——虽然它们似乎是数据库问题—— - 实际上是在您的解决方案中的其他地方引起的。

      祝你好运,希望对你有所帮助!

      【讨论】:

      • 感谢您的回答!我也考虑过这些选项,它们确实是可能的。我可能应该用这个更新我的代码示例,但我没有包括那里还有一个数据库读取。所以我要做的是 a.col1 = 'foo'; a.save(update_fields=['col1']) 然后我也做 b = A.objects.get(id=a.id) 然后记录 b.col1 它是'foo'。所以我们确实知道写入到达了数据库缓存。
      • 知道了;非常有帮助。一些额外的测试/想法:(1)如果您分别提交每个语句而不是一起提交,您是否见过相同的行为(我将其称为“提交失败”而不是“回滚”)? (2) 在您上面的“b = A.objects.get(id=a.id)”测试中,如果您在提交后立即进行相同的测试,您会看到什么?如果您在同一个数据库连接上执行此操作与打开一个新的、单独的数据库连接有什么区别吗?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-09-23
      • 2017-01-30
      • 2014-09-27
      • 1970-01-01
      • 1970-01-01
      • 2015-02-15
      相关资源
      最近更新 更多