7

私は小さなオンラインゲームを作っています。そこでは(あなたは何を知っていますか)同じデータベースにアクセスする複数のユーザーがいるでしょう。私のホストはセマフォを有効にしておらず、他の何かを買う余裕がないので(私は高校生です)、別の方法を探しています。

いくつかの調査の後、私はいくつかの回避策に出くわしました:


A)セマフォを模倣するためにいくつかの関数を作成しました。これは、私が思うに必要なだけの機能です。

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ますか?そして、あるユーザーのスクリプトがテーブルをロックし、別のユーザーがそれを要求すると、ブロックされると思いますか?私が考えることができる問題は、これがテーブル全体をロックすることですが、実際に必要なのは個々のユーザーのロックだけです(したがって、全員が待つわけではありません)。


データベースを更新するのにセマフォは必要ないと言われているようです。単純な増分クエリを使用できることがわかりましたが、解決する必要のある例がまだいくつかあります。

アクションをコミットする前に値を確認する必要がある場合はどうなりますか?たとえば、十分な体力のように、攻撃を許可する前に、防御側が十分に強いかどうかを確認する必要があるとしましょう。もしそうなら、他の誰かがデータをいじくりまわすことなく、戦いをしてダメージを与えてください。

私の理解では、コードの長さ全体にわたって毎回いくつかのクエリがあり(クエリの送信、データのフェッチ、インクリメント、保存)、データベースはそれを処理するのに十分スマートではありませんか?それとも私はここで間違っていますか?ごめん

4

1 に答える 1

26

ここでの本当の問題は、ロックを実装する方法ではなく、直列化可能性を偽造する方法です。データベース前またはデータベース以外の世界で直列化可能性を実現するように教えられる方法は、ロックとセマフォを使用することです。基本的な考え方は次のとおりです。

 lock()
 modify a bunch of shared memory
 unlock()

そうすれば、2人の同時ユーザーがいる場合、どちらも無効な状態を生成しないことを確信できます。つまり、シナリオの例は、両方のプレーヤーが同時に攻撃し、どちらが勝ったかという矛盾した概念に到達するシナリオです。したがって、この種のインターリーブについて心配しています。

  User A      User B
    |           |
    V           |
  attack!       |
    |           V
    |         attack!
    V           |
  read "wins"   |
    |           V
    |         read "wins"
    |           |
    V           |
  write "wins"  |
                V
              write "wins"

問題は、このように読み取りと書き込みをインターリーブすると、ユーザーAの書き込みが上書きされるか、その他の問題が発生することです。この種の問題は一般に競合状態と呼ばれます。これは、2つのスレッドが同じリソースを効果的に競合し、一方が「勝ち」、もう一方が「負け」、動作が意図したものではないためです。

ロック、セマフォ、またはクリティカルセクションの解決策は、一種のボトルネックを作成することです。一度に1つのタスクのみがクリティカルセクションまたはボトルネックにあるため、この一連の問題は発生しません。ボトルネックを乗り越えようとしている人は誰でも、最初に乗り越えた人を待っています。彼らはブロックしています。

 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 players SET wins = 37いるUPDATE players SET wins = wins + 1場合、データベースはこれらの更新を並行して実行できないと疑う理由はありません。その結果、より多くのデータベースfooを使用する必要があります。一貫性、つまりACIDのCについて心配する必要があります。

何か無効なことが発生したかどうかをデータベース自体が識別できるようにスキーマを設計したいと思います。可能であれば、データベースがそれを防ぐからです。:現在、私たちは設計領域にいるので、問題に取り組むためのさまざまな方法が必然的にあります。ここで紹介するのは、最高のものではなく、良いものでもないかもしれませんが、リレーショナルデータベースの問題を解決するために必要な思考プロセスを説明するものになることを願っています。

つまり、整合性、つまり、すべてのトランザクションの前後でデータベースが有効な状態にあることが重要になります。データベースがこのようにデータの有効性を処理している場合は、トランザクションを単純な方法で記述でき、同時実行性のために不合理なことを行おうとすると、データベース自体がトランザクションを中止します。これは、私たちに新しい責任があることを意味します。検証を処理できるように、データベースにセマンティクスを認識させる方法を見つける必要があります。一般に、有効性を保証する最も簡単な方法は、主キーと外部キーの制約を使用することです。つまり、行が一意であること、または行が他のテーブルの行を確実に参照していることを確認します。そこから一般化できることを期待して、ゲームの2つのシナリオの思考プロセスといくつかの代替案を紹介します。

最初のシナリオはキルです。プレーヤー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
);

これで、アトミックに新しい生活を作成できます。

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、予期しないことが発生した場合に発行したりするなどして、制約が満たされていることを手動で確認することもできます。これらをトリガーされたチェック制約にバンドルし、さらにストアドプロシージャにバンドルして、細心の注意を払うこともできます。

2番目の例に移ります:エネルギー制限操作。最も簡単な方法は、チェック制約を追加することです。

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

これで、プレイヤーがエネルギーを2回使用しようとすると、シーケンスの1つで制約違反が発生します。

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が同梱されていました。つまり、BEGIN1日中使用でき、とにかくすべてが個別に実行されていました。代わりにエンジンとしてInnoDBを使用してテーブルを作成すると、ほぼ期待どおりに機能します。可能であればPostgreSQLを試してみることをお勧めします。これは、箱から出してすぐにこのようなことについてもう少し一貫性があります。そしてもちろん、商用データベースも強力です。一方、SQLiteにはデータベース全体の書き込みロックがあるため、それがバックエンドである場合、前述のすべてはかなり意味がありません。同時書き込みシナリオにはお勧めしません。

要約すると、問題は、データベースが基本的にこの問題を非常に高レベルの方法で処理しようとしていることです。99%の場合、心配する必要はありません。正確に適切なタイミングで2つのリクエストが発生する確率は、それほど高くありません。実行したいすべてのアクションは非常に高速に実行されるため、実際の競合状態が発生する可能性はほとんどありません。しかし、それについて心配するのは良いことです。結局のところ、あなたはいつか銀行のアプリケーションを書いているかもしれません、そしてそれは物事を正しく行う方法を知ることが重要です。残念ながら、この特定のケースでは、使用しているロックプリミティブは、リレーショナルデータベースが実行しようとしているものと比較して非常にプリミティブです。しかし、データベースは、単純な馴染みのある推論ではなく、速度と整合性を最適化しようとしています。

これがあなたにとって興味深い回り道であったなら、私はあなたがジョー・セルコの本の1つまたはデータベースの教科書をチェックして詳細を確認することを望みます。

于 2012-06-15T05:57:15.763 に答える