PHP Semaphore替代?

时间:2012-06-15 05:51:44

标签: 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);
}

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

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


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


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

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

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

1 个答案:

答案 0 :(得分:24)

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

 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的写入被覆盖,或者其他一些问题。这类问题通常被称为竞争条件,因为这两个线程正在为相同的资源进行有效竞争,其中一个将赢得"赢得"另一个将"输掉" 34;并且行为不是你想要的。

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

 User A       User B
   |            |
   V            |
 attack!        |
   |          attack!
   V            |  
 lock           V
   |          blocking
   V            .
 read "wins"    .
   |            .
   V            .
 write "wins"   .
   |            .
   V            .
 unlock         V
              lock
                |
                V
               ...

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

BEGIN;

SELECT ...
UPDATE ...

COMMIT;

BEGINCOMMIT之间的所有内容都应被视为一个原子单位,因此要么全部都行,要么都不行。事实证明,依赖A对于您的特定用例是不够的,因为您的交易无法相互失败:

 User A     User B
   |          |
   V          |
 BEGIN        V
   |        BEGIN
   V          |
 SELECT ...   V
   |        SELECT ...
   V          |
 UPDATE       V
   |        UPDATE
   V          |
 COMMIT       V
            COMMIT

特别是如果你写得正确,那么数据库没有理由怀疑这些更新不能并行执行,而不是说UPDATE players SET wins = 37而不是UPDATE players SET wins = wins + 1。正在研究不同的行。因此,您需要使用更多数据库foo:您需要担心ACID中的C一致性。

我们希望设计您的架构,以便数据库本身可以识别是否发生了无效事件,因为如果可以,数据库将阻止它。 注意:既然我们已经处于设计领域,那么必然会有很多不同的方法来解决这个问题。我在这里提出的那个可能不是最好的,甚至是好的,但我希望它能说明需要继续解决关系数据库问题的思维过程。

现在我们关注 integrity ,也就是说,您的数据库在每次事务之前和之后都处于有效状态。如果数据库正在处理这样的数据有效性,您可以以天真的方式编写事务,如果数据库因并发而尝试执行不合理的操作,则数据库本身将中止它们。这意味着我们有了新的责任:我们必须找到一种方法让数据库知道你的语义,以便它可以处理验证。通常,确保有效性的最简单方法是使用主键和外键约束 - 换句话说,确保行是唯一的或者它们肯定引用其他表中的行。我将向您展示游戏中两个场景的思维过程和一些替代方案,希望您能够从那里进行概括。

第一种情况是杀戮。假设你不希望玩家1能够杀死玩家2,如果玩家2正在杀死玩家1.这意味着你希望杀戮是原子的。我这样建模:

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
);

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

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;

你可以原子地杀死:

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;

现在你可以确定这些事情是以原子方式发生的,因为UNIQUE隐含的PRIMARY KEY约束将阻止使用相同的用户和相同的生命创建新的死亡记录,无论杀手可能是谁。您还可以手动检查是否满足约束条件,例如在步骤之间发出计数语句,并在发生意外情况时发出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一起提供表格,这意味着你可以整天BEGIN,而且无论如何都要单独运行。如果你用InnoDB作为引擎来制作你的表,它会像预期的那样或多或少地工作。如果可以的话,我建议你尝试使用PostgreSQL,对于像这样开箱即用的东西,它会更加一致。当然,商业数据库也很强大。另一方面,SQLite对整个数据库都有一个写锁定,所以如果你的后端是完全没有用的话。我并不建议将它用于并发写入方案。

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

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