Django的原子操作?

Ton*_*les 24 database django concurrency locking race-condition

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

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

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

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

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

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

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

bju*_*nix 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()
Run Code Online (Sandbox Code Playgroud)

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

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


Sam*_*der 12

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


kmm*_*vnr 6

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

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

visitors = VisitorDayTypeCounter.objects.get(day=curday).for_update()
visitors.counter += 1
visitors.save()
Run Code Online (Sandbox Code Playgroud)


Dan*_*aab 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'))
Run Code Online (Sandbox Code Playgroud)

在此之后,您可能会在计数器的更新中遇到轻微的竞争条件.如果你有足够的流量来关注它,我会建议查看更精细的数据库控制的事务.我不认为ORM直接支持锁定/同步.交易文档可在此处获得.


ico*_*ast 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()
Run Code Online (Sandbox Code Playgroud)