25

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

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

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

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

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

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

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

4

7 回答 7

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

于 2009-12-23T22:35:42.773 回答
12

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

于 2008-11-11T14:51:14.103 回答
6

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

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

visitors = VisitorDayTypeCounter.objects.get(day=curday).for_update()
visitors.counter += 1
visitors.save()
于 2008-12-16T05:52:42.177 回答
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 直接支持锁定/同步。交易文件可在此处获得。

于 2008-11-11T05:56:39.907 回答
1

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

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

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

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

于 2008-11-11T06:21:04.600 回答
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()
于 2008-11-12T02:58:40.237 回答
0

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

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

于 2008-11-11T10:52:10.513 回答