插入一行并避免竞争条件(PHP / MySQL)

4
我正在开发一款多人游戏,其中有一个类似于大厅的区域,玩家可以选择“扇区”进入。大厅网关由PHP驱动,而实际的游戏过程由一个或多个Java服务器处理。数据存储在MySQL中。
正常情况下,玩家选择一个扇区并告诉大厅他想进入。大厅会检查这是否可行,包括检查该扇区的分配表中是否有太多玩家(将该扇区的入口计数与其max_players值进行比较)。然后,玩家将被添加到扇区分配表中,与该扇区配对。玩家客户端会收到一个通行证,让他连接到适当的游戏服务器。
如果两个玩家几乎同时请求访问同一个扇区,我可以想象出这样一种情况:由于在他们的检查开始时只剩下一个空位,所以他们都被添加了,从而超过了最大玩家数。
最佳解决方案是在sector_assignments上锁定表吗?还有其他选项吗?
2个回答

7
通常,解决这种并发问题的方法涉及到事务和“乐观锁”:当你更新计数器时,添加一个“where”子句来检查旧值并计算更新的行数。
v = select value from counter where id=x.
update counter set value = v+1 where value = v and id=x

如果在此期间计数器已更新,则更新不会更改任何行,因此您知道必须回滚并再次尝试事务。
一个问题是这可能导致高争用,只有少量成功的交易和大量失败的交易。
那么最好坚持悲观锁定,在先锁定行,然后更新它。但只有基准测试才能告诉你。
编辑
如果您使用没有乐观锁定的事务,则可能发生以下情况。
Max authorized = 50. Current value = 49.

T1: start tx, read value --> 49
T2: start tx, read value --> 49
T1: update value --> 50, acquire a row lock
T1: commits --> release the lock
T2: update value --> 50, acquire a row lock
T2: commits --> release the lock

两个事务都成功了,值为50,但存在不一致性。

行锁对此无效,因为计数器不是行数据的一部分,它是通过对 SECTOR 进行分组调用 sector_assignments 表上的 COUNT 确定的。看起来我需要在检查用户数量之前锁定表,然后插入数据,最后解锁。 - justkevin
我明白你的意思,计数在你的情况下是一个计算出来的值。虽然我有点固执,但如果我是你,我会在部门“max_value”旁边添加一个“current_value”列。这是数据的轻微去规范化,但它意味着:没有“count(*)”,也没有“lock”。只需使用乐观锁定,并确保在同一事务中更新“current_value”并插入“sector_assignment”行即可。 - ewernli

2
如果您使用INNODB作为存储引擎,您可以在数据库中使用事务,避免手动锁定表的需要。
在单个事务中,检查空间是否可用并将玩家添加到该区域。这将确保检查查询的结果在提交事务之前仍然有效。

1
请查看ewernli的答案,了解为什么这还不够。抱歉。 - grossvogel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接