web-dev-qa-db-ja.com

読み取りにロックせずにMySQLテーブルで非常に大規模なリアルタイム非同期書き込みを実現

私は、ユーザーが特定のアクティビティをめぐって互いに競争できるようにするWebアプリ(Angularフロントエンド、Groovy/Spring/Hibernate/MySQLバックエンド)を構築しています。各アクティビティには、勝者1人と敗者1人がいます。勝敗の記録/履歴に応じてユーザーを(1位から最後まで)ランク付けするライブユーザーランキングシステムが必要です。スコアリングとランキングは、実際には ELOレーティングシステム に基づいており、チェスコミュニティで使用されているELOのフレーバーと非常によく似ています。各ユーザーの個々のパフォーマンスレーティング/ ELOスコアの計算はかなり複雑な計算であり、これまでに獲得した勝利数を合計するほど単純ではないため、これについてのみ言及します。

また、ユーザーのランキングはデータベースに保存する必要があるものであり、単に次の方法で達成することはできません。(1)パフォーマンス評価スコアに基づいてすべてのユーザーを並べ替える、(2)並べ替えられたリストで特定のユーザーを見つける、(3)並べ替えられたリストでのランキング==位置。ランキングはDBに永続化し、頻繁に更新する必要があります。

したがって、このライブユーザーランキングシステムは次のことを行う必要があります。

  1. 2人のユーザーが互いに競争し、アクティビティが勝者/敗者を決定するたびにトリガーします。その後
  2. そのアクティビティ/競争の結果を取得し、かなり数学的に複雑なアルゴリズムを適用して、両方のユーザーの新しいパフォーマンス評価(総合スコア)を決定します。その後
  3. 一部のDBテーブルのランキングを更新します(並べ替え、最高のパフォーマンス評価が1位、最低のパフォーマンス評価が最後など)。このプロセスは再ランク付けと呼ばれ、すべてのユーザーに影響します(ユーザーを上下にシフトします)。

上記の最初の2つの項目は非常に単純です。バックエンド/ミドルウェア層で簡単に処理できます。ユーザー数が多い場合、再ランク付けには30〜60秒かかる可能性があるため、すべてのユーザーの再ランク付けとは非同期で、競争/アクティビティの結果のレポートを作成する可能性があります。つまり、バックエンドは競争結果を受け取り、それらを保存してから、再ランク付けを行う必要があるというメッセージをブローカーに公開します。次に、そのブローカーをリッスンしているコンシューマーは、再ランク付けをトリガーすることによってメッセージに反応します。

しかし、再ランク付けを実行する3番目の項目は、パフォーマンスの問題の可能性を予測する場所です。これは、私のアプリに数十万人のユーザーがいて、再ランキングの実行に最大60秒かかる場合、それらのユーザーのいずれか2人が互いに競合すると、すべてのユーザーのランキングが影響を受け、すべてのユーザーが影響を受けるためです。いくつかのランキングで上下にシフトします。 2組のユーザーが同じ時点で互いに競合し、同じ時点で複数の再ランク付けをトリガーする可能性もあります。

このシナリオでは、DBがすべてのユーザーのすべてのランキングを更新しているときに書き込み/ロックの競合が心配ですが、その間、アプリは待機状態(ランキングの更新を待機)にすることはできず、書き込まれている場合でも、ユーザー/ランキングテーブルを読み取っています。

だから私は尋ねます:再ランク付けを行うことができるように、どのようなトリック(テーブル構造または最適化、あるいはストアドプロシージャでのプログラミングトリック、またはJPA/Hibernate/JDBCデータレイヤーでの何かなど)を採用できますか?ユーザー/ランキングテーブルをロックせずにいつでも(liveユーザーランキング)?言い換えると、再ランク付けが進行中であっても、ランキングテーブルでユーザー12345のランクが45(45位)であると報告されていれば、完全に問題ありません。再ランク付けが完了すると、ユーザー12345はランク44に上がります。ブロッキング/競合を望まない。

2
smeeb

遅くなると推測する前に、実際には遅いことを実証する必要があります。これはパフォーマンス最適化の基本です

数十万人のユーザーが、適切なインデックスを設定すると、それぞれのゲーム数が1秒未満で非常に長くなります

ダミーの構造を構築し、クエリを記述し、いくつかのベンチマークを投稿すると、統計を改善する方法を確認できます。

5
Silviu-Marian

パフォーマンスのベンチマークを行う前に、これについて心配する必要があるかどうかについて、いくつかの疑問があります。ある程度は同意しますが、説明に基づいてデッドロックなどの競合の問題が発生する可能性もあります。つまり、これはパフォーマンスの問題だけではありません。この種の問題をテストしてデバッグするのは難しい場合があります。これを問題にならないように設計するのは悪い考えではないと思います。

私はMySQLに精通していないので、ロックの詳細については説明しませんが、 カーソル分離レベル に精通している必要があります。重要なのは、分離レベルが厳しくないほど、競合が少なくなることですが、結果として微妙な問題が発生する可能性もあります。管理できる最も厳しいレベルに固執することをお勧めします。

あなたは 楽観的ロック について言及します。これは、競合の多い状況で私が強くお勧めするものです。自分で決定する必要があることの1つは、アプリケーションの順序に関係なく、または気にするかどうかに関係なく、アルゴリズムが決定論的であるかどうかです。楽観的ロックは高い同時実行性を可能にするので、それが結果をどのように変えるかを理解する必要があります。

私が使用する基本的なアプローチは、各レコードにタイムスタンプまたはバージョン番号を追加することです。後者はより防弾ですが、時間分解能が十分に良好である限り、前者は機能します。次に、レコードを更新するときに、レコードを読んでから、次のように更新します。

UPDATE table_name
SET version = [incremented record version], ...
WHERE key = [surrogate key] and version = [current record version];

そして、更新されたレコードの数を確認します。更新されたレコードがゼロの場合、他の何かがレコードを更新したため、最初からやり直す必要があります。一部のデータベース/分離レベルでは、これが成功してもコミットが失敗する可能性があります。

これらの更新を一度に1つのレコードで実行する場合、更新の実行にかかる時間だけ各レコードをロックします。おそらく(あるとしても)衝突が発生することはめったにありませんが、更新によって行が更新されない場合やコミットが失敗した場合は、コードがそのレコードで最初からやり直すことを確認する必要があります。

1
JimmyJames