【问题标题】:Avoiding race conditions, Django + Heroku + PostgreSQL避免竞争条件,Django + Heroku + PostgreSQL
【发布时间】:2014-04-02 06:30:26
【问题描述】:

我正在运行一个竞赛网站,您可以在其中尝试点击数 X 来赢得奖品。它是用 Django 编写的,并在 Heroku 和 PostgreSQL 上运行。 每次点击都保存为 Play 模型的一个实例,它通过查看之前的 DB 中有多少次点击来计算它的数量,然后加 1。这个数字保存在 Play 模型中。这是整个网站的核心,因为您玩的次数决定了您是否中奖。

最近,我们遇到了一个2人同时中奖的案例。检查数据库,我发现实际上大约有 3% 的戏剧分享了他们的数字。哎呀。 我在 Play 模型的 'number' 和 'game' 字段中添加了 'unique_together',因此 DB 将帮助我避免将来出现重复的数字,但我很担心未来的比赛条件可能会使系统跳过一些数字,如果有问题的数字在哪里中奖,那就不好了。

我已经考虑锁定表,但担心这可能会破坏网站的并发性(我们目前有多达 500 名同时玩家,并且预计未来会更多)。

我应该实施什么策略来 100% 确定我从来没有重复或跳过数字?

我的游戏课:

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

【问题讨论】:

    标签: python django postgresql heroku


    【解决方案1】:

    一个简单的解决方案是将计数器和获胜者用户放入游戏模型中。然后您可以使用select_for_update 锁定记录:

    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
    

    作为同一交易的一部分,您还可以记录Player 的对象,这样您还可以知道谁点击并跟踪了其他信息,但不要将号码和获胜者放在那里。要使用select_for_update,您需要使用postgresql_psycopg2 后端。

    更新: 由于 django 默认设置自动提交,因此您必须将上述代码包装在原子事务中。来自 djangodocs

    选择更新 如果您依赖“自动事务”在 select_for_update() 和随后的 >write 操作之间提供锁定——这是一个极其脆弱的设计,但仍然可能—— 您必须将相关代码包装在 atomic() 中。

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

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

    【讨论】:

    • 谢谢,会试试的。不过,我有点困惑,因为显然我需要包装它atomic?非常感谢一个例子。
    • 谢谢!如果我把game = Game.objects.select_for_update().get(pk=gamepk)放在函数里面,当函数返回时,它的锁会被释放吗?
    • @signal 它将被锁定直到事务提交。如果您使用 @transaction.atomic 装饰器,它将在函数返回时进行,或者您可以使用 @987654330 限制原子块的范围@
    猜你喜欢
    • 1970-01-01
    • 2015-01-30
    • 2010-09-25
    • 2010-09-25
    • 2019-06-12
    • 1970-01-01
    • 2020-01-16
    • 2017-10-04
    相关资源
    最近更新 更多