django中计数器的原子增量

Bjö*_*ist 51 python django transactions race-condition

我试图在Django中以原子方式递增一个简单的计数器.我的代码看起来像这样:

from models import Counter
from django.db import transaction

@transaction.commit_on_success
def increment_counter(name):
    counter = Counter.objects.get_or_create(name = name)[0]
    counter.count += 1
    counter.save()
Run Code Online (Sandbox Code Playgroud)

如果我正确理解Django,这应该将函数包装在事务中并使增量原子化.但它不起作用,并且在计数器更新中存在竞争条件.如何使这些代码成为线程安全的?

Odu*_*van 86

Django 1.1中的新功能

Counter.objects.get_or_create(name = name)
Counter.objects.filter(name = name).update(count = F('count')+1)
Run Code Online (Sandbox Code Playgroud)

或使用F表达式:

counter = Counter.objects.get_or_create(name = name)
counter.count = F('count') +1
counter.save( update_fields=["count"] )
Run Code Online (Sandbox Code Playgroud)

请记住指定要更新的字段,或者您可能在模型的其他可能字段上遇到竞争条件!

官方文档中添加了与此方法相关竞争条件主题.

  • 这样做的一个问题是,如果之后需要更新的值,则需要从数据库中获取它.在某些情况下,如ID生成,这可能会导致竞争条件.例如,两个线程可能会原子地增加一个ID(例如从1增加到3),但是然后查询当前值并获得3,尝试插入,爆炸......只需要考虑一下. (6认同)
  • `get_or_create`返回一对,所以它应该是`counter,created = ...`就像在mlissner的答案中一样. (4认同)
  • 它应该包含在commit_on_success方法中吗? (3认同)

Emi*_*röm 17

在Django 1.4中,支持SELECT ... FOR UPDATE子句,使用数据库锁来确保不会错误地同时访问数据.


Xep*_*ous 12

如果您在设置时不需要知道计数器的价值,那么最佳答案肯定是您最好的选择:

counter = Counter.objects.get_or_create(name = name)
counter.count = F('count') + 1
counter.save()
Run Code Online (Sandbox Code Playgroud)

这告诉您的数据库将值加1 count,它可以很好地完成,而不会阻止其他操作.缺点是你无法知道count你刚刚设置了什么.如果两个线程同时命中这个函数,它们都会看到相同的值,并且都会告诉数据库添加1.数据库最终会按预期添加2,但你不会知道哪个先行.

如果您现在关心计数,可以使用select_for_updateEmil Stenstrom引用的选项.这是看起来像:

from models import Counter
from django.db import transaction

@transaction.atomic
def increment_counter(name):
    counter = (Counter.objects
               .select_for_update()
               .get_or_create(name=name)[0]
    counter.count += 1
    counter.save()
Run Code Online (Sandbox Code Playgroud)

这将读取当前值并锁定匹配的行,直到事务结束.现在只有一个工人可以一次阅读.有关select_for_update的更多信息,请参阅文档.


mli*_*ner 7

保持简单,并建立@ Oduvan的答案:

counter, created = Counter.objects.get_or_create(name = name, 
                                                 defaults={'count':1})
if not created:
    counter.count = F('count') +1
    counter.save()
Run Code Online (Sandbox Code Playgroud)

这里的优点是,如果对象是在第一个语句中创建的,则不必进行任何进一步的更新.


der*_*evo 5

Django 1.7

from django.db.models import F

counter, created = Counter.objects.get_or_create(name = name)
counter.count = F('count') +1
counter.save()
Run Code Online (Sandbox Code Playgroud)