Django 中正确处理数据库并发的方法

发布于 2024-10-06 12:22:27 字数 4111 浏览 4 评论 0

当开发对于在 web 应用外运行异步任务有特殊需求的应用的时候,通常是采用一个任务对了,例如 Celery 。这允许,例如,让服务器处理请求,启动一个异步任务来负责进行一些重量级处理,并在任务仍然运行的时候返回一个响应。

在这个例子的基础中,最理想的是将高时间要求的部分从视图处理流中分离开来,因此,我们在分离的任务重运行那些部分。现在,假设我们必须在 view 和分离的任务中进行一些数据库操作。如果不小心完成,那么那些操作会成为难以追踪的问题之源。

ATOMIC_REQUESTS 数据库配置参数

将数据库配置中的 ATOMIC_REQUESTS 参数设置为 True 是一种常见的做法。该配置允许 Django view 在一个事务中运行,这样,如果在处理请求过程中生成了异常,那么 Django 可以简单的回滚事务。这也保证了数据库永远不会处于不一致阶段,而由于事务是数据库中的一个原子操作,因此,在一个事务运行过程中,其他试图访问数据的应用不会看到来自未完成事务的不一致数据。

任务和 Django 请求处理器的数据库竞争条件

当两个或多个并发线程试图同时访问相同的内存地址(或者,在这个例子中,数据中的一些特定的数据)时,数据竞争发生了。如果,比方说,一个线程正试图读取数据,而另一个线程正试图写,或者两个线程同时在写,那么这会导致非确定结果。当然,如果两个线程只是读取数据,那么将不会发生任何问题。

现在,回到我们的问题,让我们从编写一个简单的 view 开始,它将把一些数据写入到我们的数据库中:

from django.views.generic import View
from django.http import HttpResponse
from .models import Data


class SimpleHandler(View):

    def get(self, request, *args, **kwargs):
        myData = Data.objects.create(name='Dummy')
        return HttpResponse('pk: {pk}'.format(pk=myData.pk))

这里是我们的模型:

from django.db import models


class Data(models.Model):
    name = models.CharField(max_length=50)

它塑造了一个非常简单的请求处理。如果从浏览器发起请求,那么会得到已插入数据的主键作为响应,例如 pk: 41 。现在,修改我们的 get 方法,来加载一个 celery 任务,它会从数据库中获取数据:

    def get(self, request, *args, **kwargs):
        myData = Data.objects.create(name='Dummy')
        do_stuff.delay(myData.pk)
        return HttpResponse('pk: {pk}'.format(pk=myData.pk))

而在我们的任务文件中:

from celery_test.celery import app
from .models import Data


@app.task
def do_stuff(data_pk):
    myData = Data.objects.get(pk=data_pk)
    myData.name = 'new name'

(不要忘了在你的 view 文件中导入 do_stuff !)

现在,我们有了一个竞争条件,有可能 Celery 在获取数据的时候最终引发一场,例如 Task request_handler.tasks.do_stuff[2a3aecd0-0720-4360-83b5-3558ae1472f2] raised unexpected: DoesNotExist('Data matching query does not exist.',) 。也许看起来这不应该发生,因为我们正插入一行,获取它的主键,然后将它传递给该任务。因此,匹配该查询的数据应该存在。但是,正如前面所说的,如果将 ATOMIC_REQUESTS 设置为 True ,那么该 view 将在一个事务中运行。因此,只有当该 view 完成了它的执行,并提交了事务,数据才能被外部访问。

解决方法

对于这个问题,有多种解决方法。第一个,也是最明显的方法是设置 ATOMIC_REQUESTSFalse ,但我们想要避免这样做,因为这将影响到我们项目中的其他 view,并且正如前面所说的,在请求中使用事务是由许多优势的。另一个方法是使用 non_atomic_requests 装饰器,因为这只影响到一个 view。不过,这可能不是你想要的,因为我们可以牺牲掉这一个 view 的功能。还有一些库,用于在提交当前事务的时候运行代码,例如 django_atomic_celerydjango-transaction-signals ,但那些现在已经死掉了,不应该再用它们。可以在 django-transaction-signals 项目中读到解释。

当前最常用的方法是使用钩子。对于 Django >= 1.9,你可以使用 on_commit 钩子,而对于 Django < 1.9,使用 django-transaction-hooks 。这里,我们将使用 on_commit 方法,但如果你必须使用 django-transaction-hooks,那么方法是非常类似的。

你所需做的是在你的 view 文件中,像 from django.db import transaction 这样导入 transaction ,然后传递任何你想要在提交之后执行的函数给 transaction.on_commit 。这个方法不应该有任何参数,但是我们可以将我们的任务调用封装在一个 lambda 中,因此,最后的 view 看起来像这样:

class SimpleHandler(View):

    def get(self, request, *args, **kwargs):
        myData = Data.objects.create(name='Dummy')
        transaction.on_commit(lambda: do_stuff.delay(myData.pk))
        return HttpResponse('pk: {pk}'.format(pk=myData.pk))

这个方法够简单也适合于大多数应用。如果你需要一些更具体的,那么可以使用 non_atomic_requests 装饰器。但记住,你必须手工处理回退。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

如果没结果

暂无简介

0 文章
0 评论
22 人气
更多

推荐作者

忆伤

文章 0 评论 0

眼泪也成诗

文章 0 评论 0

zangqw

文章 0 评论 0

旧伤慢歌

文章 0 评论 0

qq_GlP2oV

文章 0 评论 0

旧时模样

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文