Django中的竞态条件

50

这是一个具有潜在竞争条件的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有哪些锁定机制可用于处理类似情况?

第一次看起来,似乎你需要在该点对行进行数据库级别的锁定。我建议查阅你所使用数据库的SQL文档,并发送一个自定义查询来完成它。 - Tom Leys
1
如果可能的话,我宁愿选择一种“数据库无关”的解决方案。 - Fragsworth
1
@transaction.commit_on_success + QuerySet.select_for_update() - orokusaki
6个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
54

Django 1.4+支持select_for_update,在早期版本中,您可以执行原始SQL查询,例如select ... for update,这取决于底层数据库,将锁定该行以防止任何更新,您可以在事务结束之前对该行进行任何操作。例如:

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

看起来这个功能有很长一段时间的更新补丁 https://code.djangoproject.com/ticket/2705 - 我最近将它应用到 Django 1.3.5 上 (由于是一个难以迁移到 1.4 的大型项目) - Alex Lokk
我在想如何将这个实现作为用户类的方法(以便在其他地方重用,而不仅仅是在该视图中)。对我来说,问题在于调用代码仍然必须进行 select_for_update() 调用,但我希望它封装在用户的方法中。 - Ivan Virabyan
@IvanVirabyan 要么在 User 类中添加一个特定的方法,例如 get_user,但如果您想更通用并想要覆盖所有对象查询,请编写自定义的 ModelManager - Anurag Uniyal
请注意,Django 1.4的select for update将针对查询中所有表的行进行锁定(SQL允许您指定表的子集)-请参见https://groups.google.com/forum/#!topic/django-users/p1qnpz-S9xA。在“select_for_update()”进入Django 1.4之前,有一篇关于这种方法的好文章-https://coderanger.net/2011/01/select-for-update/。 - RichVel

23

5
F()表达式仍然不允许在更新操作中添加条件。因此,如果要增加用户积分,并且他们仍处于活动状态,则可以这样说。 - Jason Webb
不行...如果你在循环内部有更新操作,这种方法会失败! - NoobEditor
您也可以在更新操作中使用F()函数:User.objects.filter(id=user.id).update(points=F('points') + points) - Mark Mishyn

8

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


如果底层数据库(如Postgres)支持,Django 1.4的ORM现在支持悲观锁定。请参阅Django 1.4a1发布说明


7
您有多种方法来单线程处理这种事情。 一种标准的方法是“先更新”。您可以进行更新,这将占用行的独占锁;然后进行工作;最后提交更改。为使此方法有效,您需要绕过ORM的缓存。 另一种标准方法是拥有一个单线程应用程序服务器,该服务器将Web事务与复杂计算隔离开来。 您的Web应用程序可以创建一个打分请求队列,生成一个单独的进程,然后将打分请求写入此队列。生成可以放在Django的urls.py中,以便在Web应用程序启动时发生。或者可以将其放入单独的manage.py管理脚本中。或者可以在尝试第一个评分请求时根据需要执行。 您还可以使用Werkzeug创建一个单独的WSGI风格的Web服务器,通过urllib2接受WS请求。如果此服务器有一个端口号,则通过TCP/IP排队请求。如果您的WSGI处理程序只有一个线程,则已实现串行单线程。这样稍微具有可扩展性,因为评分引擎是WS请求,可以在任何地方运行。 另一种方法是拥有必须获取和保持以执行计算的其他资源。 数据库中的Singleton对象。可以使用会话ID更新唯一表中的单个行以掌控控制;使用会话ID为None更新以释放控制。必要的更新必须包括一个WHERE SESSION_ID IS NONE过滤器,以确保当锁定由其他人持有时,更新失败。这很有趣,因为它本质上是无竞争的——它是单个更新——而不是SELECT-UPDATE序列。 普通信号量可以在数据库外部使用。队列(通常)比低级信号量更易于使用。

很棒的答案。不知何故,必须对数据库行进行序列化处理,我认为队列比锁更具可扩展性。@Fragsworth:请查看此项目,其中提供了一种在Django中使用RabbitMQ的简单实现队列的方法:http://ask.github.com/celery/introduction.html - Van Gale

1
这可能过于简化您的情况,但是考虑一下仅使用JavaScript链接替换怎么样?换句话说,当用户单击链接或按钮时,将请求包装在JavaScript函数中,该函数立即禁用/“灰掉”链接并用“正在加载…”或“正在提交请求…”等信息替换文本。这对您有用吗?

2
-1 仍然不能保护网站。有时用户会使用浏览器以外的其他HTTP客户端。例如,用户可能使用wget来获取给定的URL,那么通过禁用Jscript来禁用URL将无法拯救你。如果需要,应该只使用Jscript来使页面更加用户友好,而不应该使用它来解决服务器端应用程序中的问题。 - SashaN
1
@SashaN:海报并没有说这只能通过Web浏览器访问。我们不能立即假设所有其他异常情况,例如wget。我还在答案前加了“这可能过于简化您的情况……”以涵盖异常情况,因为这个建议对许多人来说可能是一个合适的解决方案。同时,考虑到未来的问题观看者可能会有稍微不同的情况,这个答案可能正好是他们需要的。我当然不认为它应该得到“没有帮助”的投票,但我确实感谢你至少提供了一个理由。 - Wayne Koorts
1
"不要相信客户端" - Liz Av

0

现在,你必须使用:

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

1
解释一下你的意图会提高你的答案质量。 - Reporter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,