django的竞争条件

时间:2009-06-23 01:53:37

标签: python database django locking race-condition

以下是具有潜在竞争条件的django视图的简单示例:

# myapp/views.py
from django.contrib.auth.models import User
from my_libs import calculate_points

def add_points(request):
    user = request.user
    user.points += calculate_points(user)
    user.save()

竞争条件应该相当明显:用户可以两次提出此请求,并且应用程序可能同时执行user = request.user,导致其中一个请求覆盖另一个请求。

假设函数calculate_points相对复杂,并且基于各种奇怪的东西进行计算,这些东西不能放在单个update中,并且很难放入存储过程中。

所以这是我的问题:django可以使用哪种锁定机制来处理类似的情况?

6 个答案:

答案 0 :(得分:44)

Django 1.4+支持select_for_update,在早期版本中,您可以执行原始SQL查询,例如select ... for update取决于底层数据库将锁定任何更新的行,您可以使用该行执行任何操作,直到事务结束。 e.g。

from django.db import transaction

@transaction.commit_manually()
def add_points(request):
    user = User.objects.select_for_update().get(id=request.user.id)
    # you can go back at this point if something is not right 
    if user.points > 1000:
        # too many points
        return
    user.points += calculate_points(user)
    user.save()
    transaction.commit()

答案 1 :(得分:17)

从Django 1.1开始,您可以使用ORM的F()表达式来解决这个特定问题。

from django.db.models import F

user = request.user
user.points  = F('points') + calculate_points(user)
user.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

答案 2 :(得分:8)

数据库锁定是这里的方法。有计划为Django(here)添加“select for update”支持,但目前最简单的方法是在开始计算分数之前使用原始SQL来更新用户对象。


当底层数据库(如Postgres)支持它时,Django 1.4的ORM现在支持悲观锁定。请参阅Django 1.4a1 release notes

答案 3 :(得分:6)

你有很多方法可以单线程处理这种事情。

一种标准方法是更新第一。你做了一个更新,它将占用该行的独占锁;然后做你的工作;并最终提交更改。为此,您需要绕过ORM的缓存。

另一种标准方法是使用单独的单线程应用程序服务器,将Web事务与复杂计算隔离开来。

  • 您的Web应用程序可以创建评分请求队列,生成单独的进程,然后将评分请求写入此队列。产卵可以放在Django的urls.py中,所以它发生在web-app启动时。或者可以将其放入单独的manage.py管理脚本中。或者,当尝试第一个评分请求时,可以“按需”完成。

  • 您还可以使用Werkzeug创建一个单独的WSGI风格的Web服务器,它通过urllib2接受WS请求。如果此服务器具有单个端口号,则请求将按TCP / IP排队。如果您的WSGI处理程序有一个线程,那么,您已经实现了序列化单线程。这稍微可扩展,因为评分引擎是WS请求,可以在任何地方运行。

另一种方法是拥有一些必须获取并保留以进行计算的其他资源。

  • 数据库中的Singleton对象。可以使用会话ID更新唯一表中的单行以获取控制权;使用会话ID None进行更新以释放控制权。基本更新必须包含WHERE SESSION_ID IS NONE过滤器,以确保在其他人持有锁时更新失败。这很有趣,因为它本质上是无竞争的 - 它只是一次更新 - 而不是SELECT-UPDATE序列。

  • 可以在数据库外部使用花园种类的信号量。队列(通常)比低级信号量更容易使用。

答案 4 :(得分:1)

这可能会使您的情况过于简单,但仅仅更换JavaScript链接呢?换句话说,当用户点击链接或按钮将请求包装在JavaScript函数中时,该函数会立即禁用/“灰显”链接并将文本替换为“正在加载...”或“提交请求...”信息或其他内容类似。这对你有用吗?

答案 5 :(得分:0)

现在,你必须使用:

Model.objects.select_for_update().get(foo=bar)