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

0 投票
1 回答
581 浏览
提问于 2025-04-17 20:24

我正在运行一个比赛网站,用户需要点击达到某个数字X才能赢得奖品。这个网站是用Django开发的,运行在Heroku上,数据库使用的是PostgreSQL。

每次点击都会保存为一个Play模型的实例,这个模型会通过查看数据库中之前有多少次点击来计算当前的点击次数,然后加1。这个数字会保存在Play模型中。这是整个网站的核心,因为你点击的次数决定了你是否能获得奖品。

最近,我们遇到一个情况,有两个人同时达到了获胜的数字。检查数据库后发现,实际上大约有3%的点击次数是重复的。哎呀。

我在Play模型的'number'和'game'字段上添加了'unique_together',这样数据库就能帮助我避免将来出现重复的数字,但我担心未来可能会出现竞争条件,导致系统跳过某些数字。如果这些数字是获胜的数字,那就糟糕了。

我考虑过锁定表格,但担心这样会影响网站的并发性(目前我们最多有500个同时在线的玩家,未来预计会更多)。

我应该采取什么策略,才能确保绝对不会出现重复或跳过的数字呢?

我的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 个回答

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 默认开启了自动提交,所以你需要把上面的代码放在一个原子事务里。具体可以参考 Django 的 文档

选择并更新 如果你依赖“自动事务”来在 select_for_update() 和随后的写操作之间提供锁定——这是一种非常脆弱的设计,但确实是可能的—— 你必须把相关代码放在 atomic() 中。

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

from django.db import transaction

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

撰写回答