Django中计数器的原子递增
我正在尝试在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 表达式可以避免的 竞争条件,官方文档中已经添加了相关说明。