在Django竞争条件(Race conditions in django)

2019-06-23 22:12发布

这是一个潜在的竞争条件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的,对付类似这样的情况?

Answer 1:

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


Answer 2:

由于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



Answer 3:

数据库锁定是去这里的路。 有计划地增加“选择更新”来的Django(支持这里 ),但现在最简单的办法是使用原始SQL更新用户对象,你开始计算分数前。


悲观锁现在由Django的1.4的ORM支持当底层数据库(如Postgres的)支持它。 见Django的1.4a1发布说明 。



Answer 4:

你有很多方法来单线程这种事情。

一个标准的做法是首先更新 。 你做一个更新,这将抓住该行的排它锁; 然后做你的工作; 最后提交更改。 对于这个工作,你需要绕过ORM的缓存。

另一种标准方法是有一个从复杂的计算隔离Web事务的独立,单线程应用程序服务器。

  • Web应用程序可以创造出得分的请求队列,生成一个独立的过程,然后编写得分请求队列。 该菌种可以放在Django的urls.py所以它发生在web应用程序的启动。 或者,它可以被放入单独的manage.py管理脚本。 或者,它可以做到“按需”,当第一个得分王请求尝试。

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

另一种方法是有一个必须取得并保持做计算一些其他的资源。

  • 一个Singleton对象在数据库中。 在一个独特的表中的单个行可以与会话ID抓住控制进行更新; 与会话ID更新None释放控制。 基本更新必须包括一个WHERE SESSION_ID IS NONE滤波器,以确保当锁被别人持有的更新失败。 这很有趣,因为它本质上无竞争 - 这是一个更新 - 不是一个SELECT,UPDATE序列。

  • 一个花园式的各种信号可以在数据库之外使用。 队列(一般)比低级别的信号更易于使用。



Answer 5:

这可能是简单化您的情况,但对于只是一个JavaScript链接替换? 换句话说,当用户点击链接或按钮包裹在JavaScript函数,它会立即停用/“变灰”的链接,并替换文本请求“正在加载...”或“提交的请求......”信息什么的类似。 那会为你工作?



Answer 6:

现在,你必须使用:

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


文章来源: Race conditions in django