避免竞争条件,Django + Heroku + PostgreSQL [英] Avoiding race conditions, Django + Heroku + PostgreSQL

查看:113
本文介绍了避免竞争条件,Django + Heroku + PostgreSQL的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在运行一个竞赛网站,您尝试使点击数量X赢得奖品。它用Django编写,并使用PostgreSQL在Heroku上运行。
每次点击都会保存为Play模型的一个实例,该模型通过查看数据库之前的数据库数量来计算它的数量,并添加1.此数字保存在Play模型中。这是整个网站的核心,因为你做什么号码决定你是否获得奖品。



最近,我们遇到了一个案例,其中2人获胜号码在同一时间。检查数据库,我发现实际上大约有3%的玩家分享他们的数字。哎呀。
我在Play模型的' number '和' game '字段中添加了'unique_together',所以数据库将帮助我在将来避免重复的数字,但我担心未来的比赛条件可能会让系统跳过一些数字,如果数字中有中奖号码,那么这将会很糟糕。



我已经研究过锁定但是担心这可能会导致网站并发性(我们目前有多达500个同时在线的玩家,并且预计未来会有更多)。



我应该采取什么策略实现太100%肯定我从来没有重复或跳过的数字?



我的Play类:

  class Play(models.Model):
token = models.CharField(unique = True,max_length = 200)
user = models.ForeignKey(User)
game = models.ForeignKey(Game)
datetime = models.DateTimeField(auto_now_add = True)
winner = models.BooleanField(default = False)
flagged = mod els.BooleanField(default = False)
number = models.IntegerField(blank = True,null = True)
ip = models.CharField(max_length = 200,blank = True,null = True)

def assign_number(self,x = 1000):
#try在匹配条件
时分配数量最多1000次,而x> 0:
before = Play.objects.filter(id__lt = self.id,game = self.game)
try:
self.number = before.count()+ 1
self.save()
x = 0
除外:
x- = 1
$ b class Meta:
unique_together =(('user',' ('game','number'))


解决方案

一个简单的解决方案是将反击者和赢家用户放入游戏模型中。您可以使用 select_for_update 来锁定记录:

  game =游戏.objects.select_for_update()。get(pk = gamepk)
如果game.number + 1 == X
#他是赢家
game.winner = request.user
game.number = game.number + 1
game.save()

else:
#如果获胜者已经决定

作为同一事务的一部分,您还可以记录 Player s对象所以你也知道是谁点击并跟踪了其他信息,但不把数字和赢家放在那里。要使用 select_for_update ,您需要使用 postgresql_psycopg2 后端。

更新:
由于django默认设置为autocommit,所以您必须将上述代码包装在原子事务中。来自django 文档

$


选择更新
如果您依靠自动事务来提供select_for_update()和后续>写操作 - 一个非常脆弱的设计,但仍然可能 -
您必须将相关代码包装在atomic()中。

您可以用 @ transaction.atomic 来装饰您的视图:

  from django.db导入事务

@ transaction.atomic
def viewfunc(请求):
#此代码在事务中执行。
do_stuff()


I'm running a contest site where you try to make click number X to win a prize. It's written in Django and running on Heroku with PostgreSQL. Each click is saved as an instance of a Play model, which calculates it's number by seeing how many plays are in the DB before it, and adds 1. This number is saved in the Play model. This is central to the whole site, as what number play you make determines whether or not you get a prize.

Recently, we had a case where 2 people got the winning number at the same time. Checking the database, I see there's actually about a 3% of plays that share their numbers. Oops. I've added 'unique_together' to the Play model's 'number' and 'game' field, so the DB will help me avoid repeated numbers in the future, but am worried that future race conditions might make the system skip some numbers, which would be bad if the numbers in question where winning numbers.

I've looked into locking the table, but worry that this might kill the sites concurrency (we currently have up to 500 simultaneous players, and expect much more in the future).

What strategy should I implement too be 100% sure I never have repeated or skipped numbers?

My Play class:

class Play(models.Model):
    token = models.CharField(unique=True, max_length=200)
    user = models.ForeignKey(User)
    game = models.ForeignKey(Game)
    datetime = models.DateTimeField(auto_now_add=True)
    winner = models.BooleanField(default=False)
    flagged = models.BooleanField(default=False)
    number = models.IntegerField(blank=True, null=True)
    ip = models.CharField(max_length=200, blank=True, null=True)

    def assign_number(self, x=1000):
        #try to assign number up to 1000 times, in case of race condition
        while x > 0:
            before = Play.objects.filter(id__lt=self.id, game=self.game)
            try:
                self.number = before.count()+1
                self.save()
                x=0
            except:
                x-=1

    class Meta:
        unique_together = (('user', 'game', 'datetime'), ('game','number'))

解决方案

A simple solution would be putting the counter and winner user in the Game model. You can then use select_for_update to lock the record:

game = Game.objects.select_for_update().get(pk=gamepk)
if game.number + 1 == X
    # he is a winner
    game.winner = request.user
    game.number = game.number + 1
    game.save()

else:
    # u might need to stop the game if a winner already decided

As part of the same transaction you can also record Player s objects so you also know who clicked and track other info but don't put the number and winner there. To use select_for_update you need to use postgresql_psycopg2 backend.

Update: Since django set autocommit on by default, you have to wrap the above code in atomic transaction. From django docs

Select for update If you were relying on "automatic transactions" to provide locking between select_for_update() and a subsequent >write operation — an extremely fragile design, but nonetheless possible — you must wrap the relevant code in atomic().

You can decorate your view with @transaction.atomic:

from django.db import transaction

@transaction.atomic
def viewfunc(request):
    # This code executes inside a transaction.
    do_stuff()

这篇关于避免竞争条件,Django + Heroku + PostgreSQL的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆