Django的原子操作?

时间:2008-11-11 05:14:10

标签: database django concurrency locking race-condition

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

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

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

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

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

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

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

7 个答案:

答案 0 :(得分:29)

从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

答案 1 :(得分:12)

如果您确实希望计数器准确,您可以使用事务,但所需的并发数量将在任何重大负载下真正拖动您的应用程序和数据库。相反,可以考虑使用更多的消息传递方式,只需将计数记录转储到每个访问的表中,您需要增加计数器。然后,当您希望总访问次数计入访问表时。您还可以拥有一个后台进程,该进程每天运行任意次数,以便对访问进行求和,然后将其存储在父表中。为了节省空间,它还将删除它总结的子访问表中的任何记录。如果您没有多个代理商争夺相同的资源(计数器),那么您将大大减少并发成本。

答案 2 :(得分:6)

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

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

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

答案 3 :(得分:5)

两个建议:

在模型中添加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

答案 4 :(得分:1)

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

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

  1. 检查行是否存在。如果是,请抓取它。
  2. 插入行。如果没有错误,你很好,可以继续前进。
  3. 如果出现错误(即竞争条件),请重新获取该行。如果没有行,那么这是一个真正的错误。否则,你很好。
  4. 以这种方式这样做是很讨厌的,但它似乎足够快并且可以涵盖大多数情况。

答案 5 :(得分:1)

这有点像黑客。原始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()

答案 6 :(得分:0)

您应该使用数据库事务来避免这种竞争条件。通过事务,您可以在“全有或全无”基础上执行创建,读取,递增和保存计数器的整个操作。如果出现任何问题,它将回滚所有内容,您可以再试一次。

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

相关问题