Django中计数器的原子递增

75 投票
6 回答
42512 浏览
提问于 2025-04-15 15:14

我正在尝试在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()

如果我理解Django没错的话,这段代码应该会把这个函数放在一个事务中,这样增加计数的操作就会是原子的,也就是说要么全部成功,要么全部不做。但是它并没有按预期工作,导致在更新计数器时出现了竞争条件。请问如何才能让这段代码在多线程环境下安全呢?

6 个回答

18

在Django 1.4版本中,新增了对 SELECT ... FOR UPDATE 语句的支持。这种语句使用数据库锁,确保在处理数据时不会因为同时访问而出错。

20

如果你在设置计数器的时候不需要知道它的具体值,那么最好的方法就是使用上面提到的那个答案:

counter, _ = Counter.objects.get_or_create(name = name)
counter.count = F('count') + 1
counter.save()

这段代码告诉你的数据库把count的值加1,这个过程可以顺利进行,不会影响其他操作。不过缺点是,你不知道自己刚刚设置的count值是什么。如果有两个线程同时调用这个功能,它们都会看到相同的值,然后都告诉数据库加1。最终数据库会把值加到2,这样是没问题的,但你不知道哪个线程先执行的。

如果你现在需要知道count的值,可以使用Emil Stenstrom提到的select_for_update选项。它的用法如下:

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()

这段代码会读取当前的值,并在事务结束之前锁定匹配的行。这样的话,只有一个工作线程可以同时读取数据。想了解更多关于select_for_update的信息,可以查看官方文档

125

使用 F 表达式

from django.db.models import F

可以在 update() 方法中使用:

Counter.objects.get_or_create(name=name)
Counter.objects.filter(name=name).update(count=F("count") + 1)

或者在对象实例上使用:

counter, _ = Counter.objects.get_or_create(name=name)
counter.count = F("count") + 1
counter.save(update_fields=["count"])

记得要指定 update_fields,否则可能会在模型的其他字段上遇到竞争条件。

关于使用 F 表达式可以避免的 竞争条件,官方文档中已经添加了相关说明。

撰写回答