避免竞争条件,Django + Heroku + PostgreSQL

时间:2014-03-01 01:37:57

标签: python django postgresql heroku

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

最近,我们有一个案例,其中2人同时获得了中奖号码。检查数据库,我发现实际上有大约3%的游戏分享他们的数字。哎呀。 我已将“unique_together”添加到Play模型的“数字”和“游戏”字段中,因此数据库将帮助我避免将来重复使用数字,但我很担心未来的竞争条件可能会使系统跳过一些数字,如果有问题的数字在哪里获胜,那将是不好的。

我已经考虑锁定表格,但担心这可能会破坏网站的并发性(我们目前有多达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 = 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'))

1 个答案:

答案 0 :(得分: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默认设置为autocommit,因此必须将上述代码包装在原子事务中。来自django docs

  

选择更新   如果你依靠“自动事务”来提供select_for_update()和后续&gt;写操作之间的锁定 - 一个非常脆弱的设计,但仍然可能 -   你必须用atomic()包装相关的代码。

您可以使用@transaction.atomic

来装饰视图
from django.db import transaction

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