Django 1.6 事务以避免竞争条件

7 投票
1 回答
730 浏览
提问于 2025-04-18 06:00

我正在尝试使用Django 1.6的事务功能,以避免我正在开发的游戏中的竞争条件。这个游戏服务器的目标很简单:就是把两个玩家配对在一起。

我现在的做法是:

  1. 用户想要玩游戏。
  2. 服务器检查是否有其他人也在等待玩游戏。
    1. 如果没有,它就创建一个GameConnection对象(这个对象有一个唯一的标识符 - uuid4)。
    2. 如果,它就获取这个GameConnection的标识符,并删除这个GameConnection。

这段代码是:

# data['nickname'] = user's choice
games = GameConnection.objects.all()
if not games:
    game = GameConnection.objects.create(connection=unicode(uuid.uuid4()))
    game.nick1 = data["nickname"]
    game.save()

    response = HttpResponse(json.dumps({'connectionId': game.connection, 'whoAmI': 1,  'nick1': game.nick1, 'nick2': ""}))
else:
    game = games[0]
    conn = game.connection
    nick1 = game.nick1
    nick2 = data["nickname"]
    game.delete()
    response = HttpResponse(json.dumps({'connectionId': conn, 'whoAmI': 2,  'nick1': nick1, 'nick2': nick2}))

return response

显然,上面的代码存在竞争条件。因为这段代码不是原子操作,所以可能会出现以下情况:

  • A检查游戏连接,发现没有。
  • A创建了一个游戏连接。
  • B检查游戏连接,发现有一个(A)。
  • C检查游戏连接,发现有一个(A)。
  • B获取了A的连接标识符并开始了游戏。
  • C也获取了A的连接标识符并开始了游戏。

我尝试把整个代码块放在with transaction.atomic():下面,或者使用@transaction.atomic装饰器。但我仍然能重现这个竞争条件。

我确信在事务的动态处理上我遗漏了什么。有没有人能帮我解答一下?

1 个回答

2

@Sai 说得对……关键在于,锁或互斥量(mutex)不会在写入(或删除)之前发生。按照现在的代码,总会有一个时间段是在“发现”(读取)待处理连接和“声明”(写入/锁定)待处理连接之间,这个时候无法知道连接是否正在被声明。

如果你使用的是 PostgreSQL(我很确定 MySQL 也支持这个),你可以通过“select for update”来强制锁定,这样在事务完成之前,其他请求就无法获取同一行的数据:

game = GameConnection.objects.all()[:1].select_for_update()
if game:
    #do something, update, delete, etc.
else:
    #create

最后一点 - 考虑使用其他方法,而不是 all(),这样可以明确指定可能被选择的游戏(例如,可以按“创建”时间戳排序或其他方式)。希望这对你有帮助。

撰写回答