经过一些研究,我遇到了几个解决方法:
A)我做了几个模仿Semaphores的函数,这是我认为的最好:
function SemaphoreWait($id) { $filename = SEMAPHORE_PATH . $id . '.txt'; $handle = fopen($filename,'w') or die("Error opening file."); if (flock($handle,LOCK_EX)) { //nothing... } else { die("Could not lock file."); } return $handle; } function SemaphoreSignal($handle) { fclose($handle); }
(我知道if语句中有不必要的代码).你们有什么感想?显然它并不完美,但一般的做法还可以吗?有什么问题吗?这是我第一次使用并发技术,而且我通常喜欢低级语言,这使得它更有意义.
无论如何,我想不出任何“逻辑缺陷”,我唯一关心的是速度;我读过羊群很慢.
B)MysqL还提供“锁定”功能.这与鸡群相比如何?如果一个用户的脚本锁定表而另一个用户请求它,我认为它会阻塞?我能想到的问题是这会锁定整个表,但我只需要锁定个别用户(所以不是每个人都在等待).
似乎人们说我不需要信号量来更新数据库.我发现你可以使用一个简单的增量查询,但仍有一些例子我需要解决:
如果我需要在执行操作之前检查值,该怎么办?就像我们说的那样,我需要在允许攻击之前检查防守者是否足够强壮,比如足够的健康状况.如果是这样的话,那就进行战斗,并在不让其他人捣乱数据的情况下采取一些伤害.
我的理解是,每次都有一些查询,在一定长度的代码上(发送查询,获取数据,递增数据,将其存回),数据库不够智能,无法处理?或者我错了?抱歉
lock() modify a bunch of shared memory unlock()
这样,当您有两个并发用户时,您可以确定它们中的任何一个都不会产生无效状态.因此,您的示例场景是两个玩家同时互相攻击并且达成了谁赢得胜利的矛盾概念.所以你担心这种交错:
User A User B | | V | attack! | | V | attack! V | read "wins" | | V | read "wins" | | V | write "wins" | V write "wins"
问题是像这样交错读写会导致用户A的写入被覆盖,或者其他一些问题.这些类型的问题通常被称为竞争条件,因为两个线程有效竞争相同的资源,其中一个将“赢”,另一个将“失败”,行为不是你想要的.
具有锁,信号量或关键部分的解决方案是创建一种瓶颈:一次只有一个任务处于关键部分或瓶颈中,因此这组问题不会发生.每个试图克服瓶颈的人都在等待第一个完成任务的人 – 他们阻止:
User A User B | | V | attack! | | attack! V | lock V | blocking V . read "wins" . | . V . write "wins" . | . V . unlock V lock | V ...
另一种看待这种情况的方法是,需要将读/写组合视为不能被中断的单个相干单元.换句话说,它们需要原子地处理,作为原子单元.当人们说数据库是“符合ACID”时,这正是ACID中的A所代表的.在数据库中,我们没有(或者至少应该假装没有)拥有锁,因为我们改为使用描述原子单元的事务,如下所示:
BEGIN; SELECT ... UPDATE ... COMMIT;
BEGIN和COMMIT之间的所有东西都应该被视为一个原子单元,所以要么全部都行,要么都不行.事实证明,依赖A对于您的特定用例是不够的,因为您的交易无法相互失败:
User A User B | | V | BEGIN V | BEGIN V | SELECT ... V | SELECT ... V | UPDATE V | UPDATE V | COMMIT V COMMIT
特别是如果你正确地写这个,而不是说UPDATE玩家SET wins = 37你说UPDATE玩家SET wins = wins 1,数据库没有理由怀疑这些更新不能并行执行,特别是如果他们正在工作在不同的行上.因此,您将需要使用更多数据库foo:您需要担心ACID中的C一致性.
我们希望设计您的模式,以便数据库本身可以识别是否发生了无效事件,因为如果可以,数据库将阻止它.注意:既然我们处于设计领域,那么必然会有很多不同的方法来解决这个问题.我在这里提出的那个可能不是最好的,甚至是好的,但我希望它能说明需要继续解决关系数据库问题的思维过程.
所以现在我们关注的是完整性,即每个事务之前和之后数据库都处于有效状态.如果数据库正在处理这样的数据有效性,那么您可以以天真的方式编写事务,如果数据库因并发而尝试执行不合理的操作,则数据库本身将中止它们.这意味着我们有了新的责任:我们必须找到一种方法让数据库知道您的语义,以便它可以处理验证.通常,确保有效性的最简单方法是使用主键和外键约束 – 换句话说,确保行是唯一的或者它们肯定引用其他表中的行.我将向您展示思考过程以及游戏中两个场景的一些替代方案,希望您能够从那里进行概括.
第一种情况是杀戮.假设如果玩家2处于杀死玩家1的中间,你不希望玩家1能够杀死玩家2.这意味着你希望杀戮是原子的.我这样建模:
CREATE TABLE players ( login VARCHAR,-- password hashes,etc. ); CREATE TABLE lives ( login VARCHAR REFERENCES players,life INTEGER ); CREATE TABLE alive ( login VARCHAR,life INTEGER,PRIMARY KEY (login,life),FOREIGN KEY (login,life) REFERENCES lives ); CREATE TABLE deaths ( login VARCHAR REFERENCES players,killed_by VARCHAR,killed_by_life INTEGER,FOREIGN KEY (killed_by,killed_by_life) REFERENCES lives );
现在你可以原子地创造新生活:
BEGIN; SELECT login,MAX(life)+1 FROM lives WHERE login = 'login'; INSERT INTO lives (login,life) VALUES ('login','new life #'); INSERT INTO alive (login,'new life #'); COMMIT;
你可以原子地杀死:
BEGIN; SELECT name,life FROM alive WHERE name = 'killer_name' AND life = 'life #'; SELECT name,life FROM alive WHERE name = 'victim_name' AND life = 'life #'; -- if either of those SELECTs returned NULL,the victim -- or killer died in another transaction INSERT INTO deaths (name,life,killed_by,killed_by_life) VALUES ('victim','life #','killer','killer life #'); DELETE FROM alive WHERE name = 'victim' AND life = 'life #'; COMMIT;
现在你可以非常肯定这些事情是以原子方式发生的,因为PRIMARY KEY隐含的UNIQUE约束将阻止使用相同的用户和相同的生命创建新的死亡记录,无论杀手是谁.您还可以手动检查是否满足约束条件,例如在步骤之间发出计数语句,并在发生意外情况时发出ROLLBACK.您甚至可以将这些内容捆绑到触发的检查约束中,并进一步捆绑到存储过程中以实现真正细致.
到第二个例子:能量限制机动.最简单的方法是添加一个检查约束:
CREATE TABLE player ( login VARCHAR PRIMARY KEY,-- etc. energy INTEGER,CONSTRAINT ensure_energy_is_positive CHECK(energy >= 0) );
现在,如果玩家尝试两次使用他们的能量,你将在其中一个序列中遇到约束违规:
Player A #1 Player A #2 | | V | spell | | V V spell BEGIN | | | | V | BEGIN V | UPDATE SET energy = energy - 5; | | | | | V | UPDATE SET energy = energy - 5; V | [implied CHECK: pass] | | V | COMMIT V [implied CHECK: fail!] | V ROLLBACK
还有其他一些事情你可以做到将它转换为关系完整性问题而不是检查约束,例如在已识别的单位中输出能量并将它们链接到特定的法术调用实例,但它可能对你所做的事情来说太多了.干了.这将工作正常.
现在,我讨厌在将所有内容放在那里之后说出来,但是你必须检查你的数据库并确保所有内容都符合真正的ACID合规性.默认情况下,MysqL曾经随MyISAM一起提供表格,这意味着您可以全天开始,并且无论如何都要单独运行.如果你用InnoDB作为引擎制作表,它可以按预期工作或多或少.如果可以的话,我建议你试试Postgresql,开箱即用这样的东西会更加一致.当然,商业数据库也很强大.另一方面,sqlite对整个数据库都有一个写锁定,所以如果那是你的后端,那么前面的内容就没那么严重了.我不推荐它用于并发写入方案.
总之,问题是数据库从根本上试图以非常高级的方式为您处理这个问题. 99%的时间,你根本不担心;在恰当的时间发生两次请求的几率并不高.你想要采取的所有行动都将如此迅速地发生,产生实际竞争条件的可能性很小.但是担心它会很好.毕竟,有一天你可能正在编写银行应用程序,知道如何做正确的事情很重要.不幸的是,在这种特殊情况下,与关系数据库试图做的相比,您习惯使用的锁原语非常原始.但数据库正试图优化速度和完整性,而不是简单熟悉的推理.
如果这对您来说是一个有趣的绕道而行,我希望您能查看Joe Celko的书籍或数据库教科书中的更多信息.