PHP Semaphore替代?

Rae*_*kye 7 php mysql concurrency semaphore flock

我正在制作一个小型在线游戏,其中(你知道什么)会有多个用户访问同一个数据库.我的主人没有启用信号量,我无法负担其他的东西(我是一名高中生),所以我正在寻找替代方案.

经过一些研究,我遇到了几个解决方法:


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);
}
Run Code Online (Sandbox Code Playgroud)

(我知道if语句中有不必要的代码).你们有什么感想?显然它并不完美,但一般的做法还可以吗?有什么问题吗?这是我第一次使用并发技术,而且我通常喜欢低级语言,这使得它更有意义.

无论如何,我无法想到任何"逻辑缺陷",而我唯一关心的是速度; 我读过这个flock很慢.


B)MySQL还提供"锁定"功能.这比较如何flock?我假设它阻止一个用户的脚本锁定表而另一个用户请求它?我能想到的问题是这会锁定整个表,但我只需要锁定个别用户(所以不是每个人都在等待).


似乎人们说我不需要信号量来更新数据库.我发现你可以使用一个简单的增量查询,但仍然有一些我需要解决的例子:

如果我需要在执行操作之前检查值,该怎么办?就像我们说的那样,我需要在允许攻击之前检查防守者是否足够强壮,比如足够的健康状况.如果是这样的话,那就进行战斗,并在不让其他人捣乱数据的情况下造成一些伤害.

我的理解是每次都有一些查询,在一定长度的代码上(发送查询,获取数据,增加数据,将其存回),数据库不够聪明,无法处理?或者我错了?抱歉

Dan*_*ons 24

这里真正的问题不是如何实现锁,而是如何伪造可串行化.您在预数据库或非数据库世界中实现可串行化的方法是通过锁和信号量.基本的想法是这样的:

 lock()
 modify a bunch of shared memory
 unlock()
Run Code Online (Sandbox Code Playgroud)

这样,当您有两个并发用户时,您可以确定它们中的任何一个都不会产生无效状态.因此,您的示例场景是两个玩家同时互相攻击并达成谁赢得胜利的矛盾概念.所以你担心这种交错:

  User A      User B
    |           |
    V           |
  attack!       |
    |           V
    |         attack!
    V           |
  read "wins"   |
    |           V
    |         read "wins"
    |           |
    V           |
  write "wins"  |
                V
              write "wins"
Run Code Online (Sandbox Code Playgroud)

问题是像这样交错读写会导致用户A的写入被覆盖,或者其他一些问题.这些类型的问题通常被称为竞争条件,因为两个线程有​​效竞争相同的资源,其中一个将"赢",另一个将"失败",行为不是你想要的.

具有锁,信号量或关键部分的解决方案是创建一种瓶颈:一次只有一个任务处于关键部分或瓶颈中,因此这组问题不会发生.每个试图克服瓶颈的人都在等待第一个完成任务的人 - 他们阻止:

 User A       User B
   |            |
   V            |
 attack!        |
   |          attack!
   V            |  
 lock           V
   |          blocking
   V            .
 read "wins"    .
   |            .
   V            .
 write "wins"   .
   |            .
   V            .
 unlock         V
              lock
                |
                V
               ...
Run Code Online (Sandbox Code Playgroud)

另一种看待这种情况的方法是,需要将读/写组合视为不能被中断的单个相干单元.换句话说,它们需要原子地处理,作为原子单元.当人们说数据库是"符合ACID"时,这正是ACID中的A所代表的.在数据库中,我们没有(或者至少应该假装没有)拥有锁,因为我们改为使用描述原子单元的事务,如下所示:

BEGIN;

SELECT ...
UPDATE ...

COMMIT;
Run Code Online (Sandbox Code Playgroud)

之间的所有内容BEGINCOMMIT预期被视为一个原子单元,所以无论是这一切都或无的它.事实证明,依赖于A对于您的特定用例是不够的,因为您的交易无法相互失败:

 User A     User B
   |          |
   V          |
 BEGIN        V
   |        BEGIN
   V          |
 SELECT ...   V
   |        SELECT ...
   V          |
 UPDATE       V
   |        UPDATE
   V          |
 COMMIT       V
            COMMIT
Run Code Online (Sandbox Code Playgroud)

特别是如果你正确地写这个,而不是说UPDATE players SET wins = 37你说UPDATE players 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,
  life INTEGER,
  killed_by VARCHAR,
  killed_by_life INTEGER,
  PRIMARY KEY (login, life),
  FOREIGN KEY (killed_by, killed_by_life) REFERENCES lives
);
Run Code Online (Sandbox Code Playgroud)

现在你可以原子地创造新生活:

BEGIN;

SELECT login, MAX(life)+1 FROM lives WHERE login = 'login';
INSERT INTO lives (login, life) VALUES ('login', 'new life #');    
INSERT INTO alive (login, life) VALUES ('login', 'new life #');

COMMIT;
Run Code Online (Sandbox Code Playgroud)

你可以原子地杀死:

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;
Run Code Online (Sandbox Code Playgroud)

现在你可以非常肯定这些事情是以原子方式发生的,因为意志UNIQUE所暗示的约束PRIMARY KEY会阻止新的死亡记录被创造出来的同一个用户和相同的生命,无论杀手是谁.您还可以手动检查是否满足约束条件,例如在步骤之间发出计数语句,并ROLLBACK在发生意外情况时发出.您甚至可以将这些内容捆绑到触发的检查约束中,并进一步捆绑到存储过程中以获得非常细致的信息.

到第二个例子:能量限制机动.最简单的方法是添加一个检查约束:

CREATE TABLE player (
  login VARCHAR PRIMARY KEY, -- etc.
  energy INTEGER,
  CONSTRAINT ensure_energy_is_positive CHECK(energy >= 0)
);
Run Code Online (Sandbox Code Playgroud)

现在,如果玩家尝试两次使用他们的能量,你会在其中一个序列中遇到约束违规:

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
Run Code Online (Sandbox Code Playgroud)

还有其他一些事情你可以做到将它转换为关系完整性问题而不是检查约束,例如在已识别的单位中输出能量并将它们链接到特定的法术调用实例,但它可能对你所做的事情来说太多了.干嘛 这将工作正常.

现在,我讨厌不得不铺设了这一切,有后这样说,但你必须检查你的数据库,并确保一切都建立真正符合ACID.默认情况下,MySQL曾经随MyISAM一起提供表格,这意味着您可以BEGIN整天运行,而且无论如何都要单独运行.如果你用InnoDB作为引擎制作表,它可以按预期工作或多或少.如果可以的话,我建议你尝试使用PostgreSQL,对于像这样开箱即用的东西,它会更加一致.当然,商业数据库也很强大.另一方面,SQLite对整个数据库都有一个写锁定,所以如果那是你的后端,那么前面的内容就没那么严重了.我不推荐它用于并发写入方案.

总之,问题是数据库从根本上试图以非常高级的方式为您处理这个问题.99%的时间,你根本不担心; 在恰当的时间发生两次请求的几率并不高.你想要采取的所有行动都将如此迅速地发生,产生实际竞争条件的可能性很小.但是担心它会很好.毕竟,有一天你可能正在编写银行应用程序,知道如何做正确的事情很重要.不幸的是,在这种特殊情况下,你已经习惯了锁原语相比有什么关系数据库正在尝试做的非常原始.但数据库正试图优化速度和完整性,而不是简单熟悉的推理.

如果这对您来说是一个有趣的绕道而行,我希望您能查看Joe Celko的书籍或数据库教科书中的更多信息.

  • @Raeki - 我稍微扩展了答案.如果有帮助,请告诉我. (2认同)