【问题标题】:Atomic operations in Django?Django中的原子操作?
【发布时间】:2010-09-21 18:13:10
【问题描述】:

我正在尝试为计数器实现(我认为是)一个非常简单的数据模型:

class VisitorDayTypeCounter(models.Model):
    visitType = models.CharField(max_length=60)
    visitDate = models.DateField('Visit Date')
    counter = models.IntegerField()

当有人通过时,它会寻找与 visitType 和 visitDate 匹配的行;如果该行不存在,将使用 counter=0 创建。

然后我们增加计数器并保存。

我担心这个过程完全是一场竞赛。两个请求可以同时检查实体是否存在,并且它们都可以创建它。在读取计数器和保存结果之间,可能会出现另一个请求并增加它(导致计数丢失)。

到目前为止,无论是在 Django 文档中还是在教程中,我都没有真正找到解决这个问题的好方法(实际上,看起来教程的投票部分有竞争条件)。

我如何安全地做到这一点?

【问题讨论】:

    标签: database django concurrency locking race-condition


    【解决方案1】:

    从 Django 1.1 开始,您可以使用 ORM 的 F() 表达式。

    from django.db.models import F
    product = Product.objects.get(name='Venezuelan Beaver Cheese')
    product.number_sold = F('number_sold') + 1
    product.save()
    

    有关更多详细信息,请参阅文档:

    https://docs.djangoproject.com/en/1.8/ref/models/instances/#updating-attributes-based-on-existing-fields

    https://docs.djangoproject.com/en/1.8/ref/models/expressions/#django.db.models.F

    【讨论】:

    • 酷!这是一个很棒的方法。要是我在做这个项目的时候有这个就好了。
    • 对于现代 Django 安装,这是正确的答案,应该由 OP 反映。
    【解决方案2】:

    如果您真的希望计数器准确,您可以使用事务,但所需的并发量确实会在任何重大负载下拖累您的应用程序和数据库。而是考虑采用更多消息传递样式的方法,并将计数记录转储到您想要增加计数器的每次访问的表中。然后,当您想要访问总数时,请在访问表上进行计数。您还可以有一个每天运行任意次数的后台进程来汇总访问次数,然后将其存储在父表中。为了节省空间,它还会从它汇总的子访问表中删除所有记录。如果您没有多个代理争夺相同的资源(柜台),您将大大减少并发成本。

    【讨论】:

    • 您好!我一直在做 App Engine 的工作,我被“事务只作用于一个条目”和“做聚合函数非常昂贵”所困扰。这是解决问题的一种非常简单的方法。谢谢!
    • 我想它确实取决于进程是重读还是重写。在我的系统中,计数的读取频率将比它们增加的频率高得多,因此对于上述问题,这可能不是最好的计划。不过,它确实解决了我的其他顾虑,非常感谢!
    • 根据允许计数的陈旧程度,您可以有一个后台进程每隔一段时间对它们进行汇总。那么你就不会根据请求进行聚合。
    • 有趣的是,这种日志记录方法类似于在许多数据库中如何使事务安全、原子和可恢复(例如,请参阅 Postgresql 手册中的实现细节)。
    【解决方案3】:

    您可以使用来自http://code.djangoproject.com/ticket/2705 的补丁来支持数据库级锁定。

    使用补丁,此代码将是原子的:

    visitors = VisitorDayTypeCounter.objects.get(day=curday).for_update()
    visitors.counter += 1
    visitors.save()
    

    【讨论】:

    • 这太酷了。当我第一次提出这个问题时(3 年前!),我没有看到这一点!
    【解决方案4】:

    两个建议:

    为您的模型添加一个 unique_together,并将创建包装在异常处理程序中以捕获重复项:

    class VisitorDayTypeCounter(models.Model):
        visitType = models.CharField(max_length=60)
        visitDate = models.DateField('Visit Date')
        counter = models.IntegerField()
        class Meta:
            unique_together = (('visitType', 'visitDate'))
    

    在此之后,您可能会在计数器更新时遇到较小的竞争条件。如果您有足够的流量来担心这一点,我建议您研究事务以进行更细粒度的数据库控制。我不认为 ORM 直接支持锁定/同步。交易文档可在here 获得。

    【讨论】:

    • unique_together 确实让我感觉更舒服一些。可能永远不会有足够的流量来导致比赛受到影响,但由于我同时学习 Django,我想我想“做对”。感谢您的帮助!
    • 是的,我听到了。也许这里的其他人会知道用于处理此问题的 ORM 功能,或者如果某些内置函数对这种情况是安全的,则可以清除。
    【解决方案5】:

    这有点小题大做。原始 SQL 将使您的代码的可移植性降低,但它会摆脱计数器增量的竞争条件。理论上,这应该在您进行查询时增加计数器。我还没有对此进行测试,因此您应该确保该列表正确插入到查询中。

    class VisitorDayTypeCounterManager(models.Manager):
        def get_query_set(self):
            qs = super(VisitorDayTypeCounterManager, self).get_query_set()
    
            from django.db import connection
            cursor = connection.cursor()
    
            pk_list = qs.values_list('id', flat=True)
            cursor.execute('UPDATE table_name SET counter = counter + 1 WHERE id IN %s', [pk_list])
    
            return qs
    
    class VisitorDayTypeCounter(models.Model):
        ...
    
        objects = VisitorDayTypeCounterManager()
    

    【讨论】:

    • 数据库是否仍然有可能同时通过两个单独的连接执行此查询并且仍然具有(低得多的概率)竞争条件?这一切都取决于使操作原子化的连接层所隐含的围绕该查询的隐藏事务。
    • 如果您观看 DjangoCon 的“为什么我讨厌 Django”主题演讲,这种类型的查询是作为一种适当的、无竞争条件的方式在 SQL 中进行增量(问题在于 Django 的 ORM不能为你做)。
    • 我会看你的幻灯片......你几乎证实了我的怀疑,即 ORM 不会自己做。感谢您的帮助!
    • 如果您查看 Django Ticket 7210 –“为 QuerySet.update 添加了表达式支持”,似乎帮助正在使用 ORM 以可移植方式执行此操作...跨度>
    • 这个答案可能曾经有效,但现在已经过时了很长时间。从 Django 1.1 开始,Django 的 ORM 直接支持这一点。
    【解决方案6】:

    为什么不使用数据库作为并发层?将表的主键或唯一约束添加到 visitType 和 visitDate。如果我没记错的话,django 在他们的数据库模型类中并不完全支持这一点,或者至少我没有看到一个例子。

    将约束/键添加到表后,您所要做的就是:

    1. 检查该行是否存在。如果是,请获取它。
    2. 插入行。如果没有错误,您就可以继续前进。
    3. 如果出现错误(即竞争条件),请重新获取该行。如果没有行,那么这是一个真正的错误。否则,你很好。

    这样做很讨厌,但它似乎足够快并且可以涵盖大多数情况。

    【讨论】:

    • 不处理两个人同时去更新计数器的情况。
    【解决方案7】:

    您应该使用数据库事务来避免这种竞争情况。事务允许您在“全有或全无”基础上执行创建、读取、递增和保存计数器的整个操作。如果出现任何问题,它将回滚整个事情,您可以重试。

    查看 Django docs. 有一个事务中间件,或者你可以在视图或方法周围使用装饰器来创建事务。

    【讨论】:

    • 我同意事务似乎是这里的答案,但尚不清楚该功能是否会真正解决增量问题——获取行的 SELECT 仍然会成功,而更改值的 UPDATE计数器仍然会成功。如果我错了,一个例子会很棒。
    • 您需要在选择期间锁定表格才能这样做,正如 Sam 所说,这会拖累您的表现。如果您不经常增加计数器,这是最好的方法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多