ホームページ  >  記事  >  データベース  >  Redis と MySQL の間の二重書き込みの整合性の問題を解決する方法

Redis と MySQL の間の二重書き込みの整合性の問題を解決する方法

王林
王林転載
2023-06-03 12:28:101323ブラウズ

    Redis と MySQL 間の二重書き込みの整合性とは、キャッシュとデータベースを使用してデータを同時に保存する シナリオを指します (主に、高い同時実行性) 2 つの間のデータの一貫性を確保する方法 (コンテンツが同じか、可能な限り近いもの)

    通常のビジネス プロセス :

    Redis と MySQL の間の二重書き込みの整合性の問題を解決する方法

    読み取りは問題ありません。問題は書き込み操作 (更新) にあります。 。この時点でいくつかの問題が発生する可能性があるため、最初にデータベースを更新してからキャッシュ操作を実行する必要があります。キャッシュを扱うときは、キャッシュを更新するか、キャッシュを削除するか、または最初にキャッシュを更新してからデータベースを更新するかを検討する必要があります。

    要約すると、最初にキャッシュを操作し、次にデータベースを操作する必要があります。それとも最初にデータベースを操作してからキャッシュを操作しますか?

    質問を続けましょう。

    まず、操作キャッシュについて説明します。これには、更新キャッシュと削除キャッシュの 2 種類があります。選択方法は?

    キャッシュを更新しますか?キャッシュを削除しますか?

    データベースが最初に更新されると仮定します (最初にキャッシュを操作してからデータベースを操作することは大きな問題となるため、これについては後で説明します)

    • キャッシュの更新

    最初にデータベースを更新し、次にキャッシュを更新します。

    2 つのリクエストが同じデータを同時に変更する場合、順序が逆になる可能性があるため、古いデータがキャッシュに存在する可能性があります。後続の読み取りリクエストでは古いデータが読み取られ、キャッシュが無効化された場合にのみデータベースから正しい値を取得できます。

    Redis と MySQL の間の二重書き込みの整合性の問題を解決する方法

    • #キャッシュの削除

    まずデータベースを更新してからキャッシュを削除してください。

    キャッシュが失敗すると、リクエスト B はデータベースからデータをクエリし、古い値を取得する可能性があります。このとき、A はデータベースを更新し、新しい値をデータベースに書き込み、キャッシュを削除するように要求されます。そして、リクエスト B は古い値をキャッシュに書き込むため、ダーティ データが生成されます

    Redis と MySQL の間の二重書き込みの整合性の問題を解決する方法

    #上記のことから、ダーティ データの要件は、キャッシュを更新する場合、次の条件を満たす必要があります:

    1. #キャッシュの無効化

    2. #読み取りの同時実行リクエストと書き込みリクエスト
    3. データベースの更新とキャッシュの削除にかかる時間
    4. は、データベースの読み取りと書き込みにかかる時間

      よりも短いです。キャッシュ

      最初の 2 つは非常に満足です。3 つ目のポイントを見てみましょう。これは本当に起こるでしょうか?
    データベースは通常、更新時にロックされ、読み取り操作は書き込み操作よりもはるかに高速であるため、3 番目のポイントが発生する確率は非常に低いです (もちろん、発生する可能性はあります)

    注:これがよく分かりませんが、単純に言うと発生確率は低いですが、ネットワーク遅延などがあれば発生するのではないでしょうか?善意のある誰かが混乱を解消してくれるといいのですが、とにかく私には理解できません。

    したがって、キャッシュの削除を選択する場合は、他のテクノロジーを組み合わせてパフォーマンスと一貫性を最適化する必要があります。例:

    メイン スレッドのブロックやメッセージの損失を避けるために、メッセージ キューを使用してキャッシュを非同期的に削除または更新します。
    • 遅延二重削除を使用して、削除の成功率を高め、一貫性のない時間枠を短縮します。つまり、データベースが更新された直後にキャッシュが一度クリアされ、一定時間後に再度キャッシュがクリアされます。
    • 比較

    更新キャッシュでは、毎回キャッシュが更新されますが、キャッシュ内のデータが

    read にならない場合があります。
    を使用すると、アクセス頻度の低いデータが大量にキャッシュに保存されることになり、キャッシュ リソースが無駄になります。また、多くの場合、キャッシュに書き込まれる値はデータベース内の値と 1 対 1 に対応していません。最初にデータベースにクエリが実行され、その後、一連の「計算」を通じて値が取得される可能性が非常に高いです。値がキャッシュに書き込まれる前。

    この 更新キャッシュ

    スキームは、キャッシュ使用率が低いだけでなく、マシンのパフォーマンスの浪費を引き起こすことがわかります。したがって、一般的には、

    キャッシュの削除最初にキャッシュを更新し、次にデータベースを更新します

    データを更新するときは、

    新しいデータを次の場所に書き込みます。最初にキャッシュ (Redis )、次に新しいデータをデータベース (MySQL) に書き込みます

    しかし、いくつかの問題があります:

      キャッシュの更新は成功しましたが、データベースの更新に失敗したため、データの不整合が発生しました
    : ユーザーがニックネームを変更しました。最初にシステムが変更されました。新しいニックネームをキャッシュに書き込み、データベースを更新します。しかし、データベースの更新処理中にネットワーク障害やデータベースのダウンタイムなどの異常事態が発生し、データベース内のニックネームが変更されませんでした。このようにして、キャッシュ内のニックネームはデータベース内のニックネームと不一致になります。

    • キャッシュの更新は成功しましたが、データベースの更新が遅れているため、他のリクエストが古いデータを読み取ることになります

    : ユーザーが製品を注文すると、システムはまず注文ステータスをキャッシュに書き込み、次にデータベースを更新します。ただし、データベースの更新処理中は、同時実行性が高いなどの理由により、データベースの書き込み速度がキャッシュの書き込み速度よりも遅くなります。このように、他のリクエストは、キャッシュからは注文ステータスを支払い済みとして読み取りますが、データベースからは注文ステータスを未払いとして読み取ります。

    • キャッシュの更新は成功しましたが、データベースが更新される前に他のリクエストがキャッシュとデータベースにクエリを実行したため、古いデータがキャッシュに書き戻され、新しいデータが上書きされました。

    例: ユーザー A は自分のアバターを変更し、サーバーにアップロードしました。システムはまず新しいアバター アドレスをキャッシュに書き込み、それを表示のためにユーザー A に返します。次に、新しいアバターのアドレスをデータベースに更新します。しかし、このプロセス中に、ユーザー B はユーザー A の個人ホームページにアクセスし、キャッシュから新しいアバター アドレスを読み取りました。キャッシュの無効化は、キャッシュの有効期限ポリシー、またはキャッシュがクリアまたは期限切れになる再起動操作などのその他の理由が原因である可能性があります。このとき、ユーザ B は、ユーザ A の個人ホームページに再度アクセスし、データベースから古いアバターのアドレスを読み出し、キャッシュに書き戻します。これにより、キャッシュ内のアバターのアドレスがデータベース内のアドレスと一致しない可能性があります。

    上記で多くのことを述べてきましたが、要約すると、 キャッシュの更新は成功しましたが、データベースは更新されませんでした (更新に失敗しました) その結果、キャッシュには最新の値と古い値を保存するデータ インベントリ。キャッシュが失敗すると、データベース内の古い値が取得されます。

    私も後で混乱しましたが、問題の原因はデータベースの更新の失敗であるため、データベースの更新が成功していることを確認できれば、データの不整合の問題は解決できますか?更新に失敗しました。データベースの更新が完了するまで、データベースの更新を再試行してください。

    後で、私が甘すぎたことがわかり、次のような多くの問題が発生しました。

    • データベース更新の失敗の理由がデータベースのダウンタイムまたはネットワーク障害である場合データベースの更新を再試行すると、さらなるストレスや遅延が発生したり、データベースの回復が困難になったりする可能性があります。

    • データベース更新の失敗の理由がデータの競合またはビジネス ロジック エラーである場合、データベースの更新を頻繁に再試行すると、データの損失やデータの混乱が発生し、さらには他のユーザーのデータに影響を与える可能性があります。 . .

    • データベースの更新を再試行し続ける場合は、再試行の冪等性と順序を確保する方法、および再試行プロセス中に発生する例外を処理する方法を検討する必要があります。

    つまり、この方法は良い解決策ではありません。

    最初にデータベースを更新してからキャッシュを更新します

    更新操作がある場合は、最初にデータベース データを更新してから、対応するキャッシュ データを更新します

    ただし、このソリューションには次のようないくつかの問題とリスクもあります。

    • データベースは正常に更新されたが、キャッシュの更新が失敗した場合、古いデータはキャッシュ: データベースにはすでに新しいデータ、つまりダーティ データが存在します。

    • データベースの更新とキャッシュの更新の間に別のリクエストが同じデータをクエリし、キャッシュが存在することが判明した場合、古いデータがキャッシュから読み取られます。これにより、キャッシュとデータベース間の不整合も発生します。

    したがって、更新キャッシュ操作を使用する場合、誰が先でも後でも、後者で例外が発生するとビジネスに影響を及ぼします。 。 (まだ上の図)

    Redis と MySQL の間の二重書き込みの整合性の問題を解決する方法

    #データの一貫性を確保するために例外を処理する方法

    ソースこれらの問題のうちの 1 つはマルチスレッドの同時実行によって発生するため、最も簡単な方法はロック (分散ロック) を追加することです。 2 つのスレッドが同じデータを変更する場合、各スレッドは変更を行う前に分散ロックを適用する必要があります。ロックを取得したスレッドのみがデータベースとキャッシュを更新できます。ロックを取得できないスレッドは失敗を返し、次の再試行を待ちます。この理由は、同時実行性の問題を防ぐために、データとキャッシュの操作を 1 つのスレッドのみに制限するためです。

    ただし、ロックには時間と労力がかかるため、絶対にお勧めできません。また、キャッシュが更新されるたびに、キャッシュ内のデータがすぐに読み出されないため、アクセス頻度の低いデータが大量にキャッシュに格納され、キャッシュリソースが無駄に消費される可能性があります。また、多くの場合、キャッシュに書き込まれる値はデータベース内の値と 1 対 1 に対応していません。最初にデータベースにクエリが実行され、その後、一連の「計算」を通じて値が取得される可能性が非常に高いです。値がキャッシュに書き込まれる前。

    データベースを更新してキャッシュを更新するこのスキームでは、キャッシュの使用率が低いだけでなく、マシンのパフォーマンスの無駄が発生することがわかります。

    したがって、現時点では別のオプションを検討する必要があります: キャッシュを削除します

    最初にキャッシュを削除してからデータベースを更新します

    更新操作がある場合、最初に対応するキャッシュ データを削除してから、データベース データを更新します

    ただし、この解決策には次のようないくつかの問題とリスクもあります。

    • キャッシュの削除後にデータベースの更新が失敗した場合、キャッシュは失われ、次のクエリ時にデータベースからデータを再ロードする必要があるため、データベースの負荷と応答時間が増加します。

    • キャッシュを削除してからデータベースを更新するまでの間に、同じデータに対する他のリクエストがあり、キャッシュが存在しないことが判明した場合は、古いデータがデータベースから読み取られます。データベースとキャッシュへの書き込み。これにより、キャッシュとデータベースの間で不整合が発生します。

    Redis と MySQL の間の二重書き込みの整合性の問題を解決する方法

    #最初にデータベースを更新してからキャッシュを削除します

    更新操作がある場合は、データベースのデータを更新します最初にキャッシュを削除します。

    実際には上で述べましたが、もう一度繰り返します。

    キャッシュが失敗すると、リクエスト B はデータベースからデータをクエリして古い値を取得する可能性があります。 。このとき、A はデータベースを更新し、新しい値をデータベースに書き込み、キャッシュを削除するように要求されます。そして、リクエスト B は古い値をキャッシュに書き込むため、ダーティ データが生成されます

    Redis と MySQL の間の二重書き込みの整合性の問題を解決する方法

    #上記のことから、ダーティ データの要件は、キャッシュを更新する場合、次の条件を満たす必要があります:

    1. #キャッシュの無効化

    2. #読み取りの同時実行リクエストと書き込みリクエスト
    3. データベースの更新とキャッシュの削除にかかる時間
    4. は、データベースの読み取りと書き込みにかかる時間

      よりも短いです。キャッシュ

      最初の 2 つは非常に満足です。3 つ目のポイントを見てみましょう。これは本当に起こるでしょうか?
    データベースは通常、更新時にロックされ、読み取り操作は書き込み操作よりもはるかに高速であるため、3 番目のポイントが発生する確率は非常に低くなります。

    二重の問題に対するより適切な解決策-write の問題 解決策は、データベースを更新した後にキャッシュを削除することです もちろん、特定の状況には特定の分析が必要であり、一般化することはできません。

    これらの操作後に発生する問題について説明しましたが、これらの問題を回避するにはどうすればよいですか?

    最初にキャッシュを削除し、次にデータベースを更新してから、非同期スレッドまたはメッセージ キューを使用してキャッシュを再構築します。
    • 最初にデータベースを更新してからキャッシュを削除し、キャッシュの有効性を確保するために適切な有効期限を設定します。
    • 分散ロックまたはオプティミスティック ロックを使用して同時アクセスを制御し、一度に 1 つのリクエストだけがキャッシュとデータベースを操作できるようにします
    • &hellip ; …

    二重書き込みの一貫性を確保するための一般的な方法をいくつか紹介します

    解決策

    1.もう一度お試しください

    As上で述べたように、第 2 段階の操作が失敗した場合は、再試行して可能な限り修復しようとしますが、再試行のコストが高すぎるため、上で述べたことは繰り返しません。

    2. 非同期再試行

    再試行メソッドはリソースを消費するため、非同期で実行します。キャッシュを削除または更新するときに、操作が失敗した場合、エラーはすぐに返されず、代わりに、いくつかのメカニズム (メッセージ キュー、スケジュールされたタスク、バイナリログ サブスクリプションなど) を通じてキャッシュの再試行操作がトリガーされます。この方法では、キャッシュを同期的に再試行する際のパフォーマンスの低下やブロッキングの問題を回避できますが、キャッシュとデータベースのデータが不一致になると時間が長くなります。

    2.1 メッセージ キューを使用して再試行を実装する

      #メッセージ キューにより信頼性が確保されます
    • : キューに書き込まれたメッセージは、正常に完了するまで失われません。消費済み (プロジェクトの再起動について心配する必要はありません)

    • メッセージ キューはメッセージ配信の成功を保証します
    • : ダウンストリームはキューからメッセージをプルし、メッセージのみを削除します正常に消費された後、それ以外の場合は、メッセージを消費者に配信し続けます (再試行要件に従って)

    • Redis と MySQL の間の二重書き込みの整合性の問題を解決する方法

    • メッセージキューを使用して非同期に再試行します キャッシュ状況とは、情報が変更されると、最初にデータベースが更新され、その後キャッシュが削除されることを意味します。削除が成功すると、全員が満足します。削除が失敗した場合、必要なキーは削除対象のメッセージはメッセージキューに送信されます。さらに、コンシューマ スレッドは、削除するキーをメッセージ キューから取得し、そのキーに基づいて Redis キャッシュを削除または更新します。操作が失敗した場合、操作はメッセージ キューに再送信され、再試行されます。

    注: 最初にメッセージを削除せずにメッセージ キューに直接送信し、メッセージ キューをそのままにすることもできます。ユーザー情報テーブルです。ユーザー情報を Redis に保存したいと考えています。以下に、メッセージ キューの非同期再試行キャッシュを使用したソリューションを例として、実行できる手順を示します。
    • ユーザー情報が変更された場合は、まずデータベースを更新し、成功した結果をフロントエンドに返します。

    • キャッシュを削除してみます。成功すると、操作は終了します。失敗した場合は、キャッシュの削除または更新操作に関するメッセージが生成されます (たとえば、キーと操作タイプ) に基づいてメッセージ キューに送信されます (Kafka や RabbitMQ の使用など)。

    • 別のコンシューマ スレッドは、メッセージ キューからこれらのメッセージをサブスクライブして取得し、メッセージの内容に基づいて Redis 内の対応する情報を削除または更新します。

    • キャッシュが正常に削除または更新されると、操作の繰り返しを避けるために、メッセージはメッセージ キューから削除 (破棄) されます。

    • キャッシュの削除または更新が失敗した場合は、遅延時間や再試行制限を設定するなどの失敗戦略を実装し、再試行のためにメッセージをメッセージ キューに再送信します。

    • 一定回数の再試行が失敗した場合、エラー メッセージがビジネス層に送信され、ログに記録されます。

    2.2 binlog は非同期再試行削除を実装します

    binlog を使用して一貫性を実現する基本的な考え方は、binlog ログを使用してデータベース変更操作を記録し、その後、メインを使用します。レプリケーションまたは増分バックアップからデータを同期または復元します。

    たとえば、マスター データベースとスレーブ データベースがある場合、マスター データベースで binlog を有効にし、スレーブ データベースをそのレプリケーション ノードとして設定できます。このようにして、マスター データベースで変更操作が発生すると、対応する binlog ログがスレーブ データベースに送信され、スレーブ データベースは binlog ログに基づいて同じ操作を実行してデータの一貫性を確保します。

    さらに、特定の時点より前のデータを復元する必要がある場合は、binlog ログを使用してこれを実現することもできます。まず、対応する時点より前の最新の完全バックアップ ファイルを見つけて、それをターゲット データベースに復元する必要があります。次に、対応する時点より前のすべての増分バックアップ ファイル (つまり、binlog ログ ファイル) を検索し、それらをターゲット データベースに順番に適用する必要があります。このようにして、目標時点以前のデータ状態を復元できます。

    Redis と MySQL の間の二重書き込みの整合性の問題を解決する方法

    • Binlog を使用して、Redis キャッシュをリアルタイムで更新/削除します。 Canal を使用すると、キャッシュの更新を担当するサービスが MySQL スレーブ ノードに偽装され、MySQL から Binlog を受信し、Binlog を解析して、リアルタイムのデータ変更情報を取得し、変更情報に基づいて Redis キャッシュを更新/削除します。

    • ##MQ Canal 戦略は、Canal Server が受信した Binlog データを直接 MQ に配信して分離し、MQ を使用してデータ同期のために Binlog ログを非同期的に使用します。

    MySQL の binlog ログには、挿入、更新、削除などのデータベース変更操作が記録されます。 binlog ログには 2 つの主な機能があります。1 つはマスター/スレーブ レプリケーションで、もう 1 つは増分バックアップです。

    マスター/スレーブ レプリケーションは、マスター データベースから 1 つ以上のスレーブ データベースにデータを同期することによってデータ同期を実現するプロセスです。マスター データベースは独自の binlog ログをスレーブ データベースに送信し、スレーブ データベースはデータの一貫性を確保するために binlog ログに基づいて同じ操作を実行します。このアプローチを実装することで、データの可用性と信頼性が向上し、負荷分散と障害回復が実現できます。

    増分バックアップとは、完全バックアップに基づいたデータベースの変更の定期的なバックアップを指します。フルバックアップとは、データベース全体のデータをファイルに完全にバックアップすることを指します。目的は、データベース内の最新の変更を以前のバックアップとマージして、最新の状態に復元することです。そうすることで、バックアップに必要なスペースと時間を節約できるだけでなく、任意の時点へのデータの復元も簡単になります。

    現時点では、データベースとキャッシュの一貫性を確保するには、「最初にデータベースを更新してからキャッシュを削除する」ソリューションを採用し、協力することが推奨されると結論付けることができます。 「メッセージキュー」または「変更ログの購読」を使用する」方法です。

    3. 遅延二重削除

    私たちの焦点は、最初にデータベースを更新してからキャッシュを削除することです。最初にキャッシュを削除してからデータベースを更新したい場合はどうすればよいですか?

    前に述べたことを思い出してください: まずキャッシュを削除してから、データベースを更新します。古い値がキャッシュを上書きするという問題が発生します。これは簡単に対処できます。古い値を削除するだけで終わりです。遅延二重削除 これが原則です。基本的な考え方は次のとおりです:

    1. 最初にキャッシュを削除します

    2. 次にデータベースを更新します

    3. 一定時間スリープします (システムの状況に応じて決定されます)

    4. #キャッシュを再度削除します

    データベースの更新後に他のスレッドが読み取ってしまうのを避けるため、キャッシュされたデータを期限切れにしてキャッシュに書き戻すことでデータの不整合が発生するという対策がとられました。

    例: ユーザー情報テーブルがあり、その 1 つがユーザー ポイントであるとします。現在、2 つのスレッド A と B が同時にユーザー ポイントを操作しています:

    • スレッド A はユーザーに 100 ポイントを追加したいと考えています

    • # # スレッド B はユーザーに 50 ポイントを減らそうとしています

    遅延二重削除戦略が使用される場合、スレッド A と B の実行プロセスは次のようになります。

    • スレッド A が最初にユーザー情報を削除します。キャッシュ

    • スレッド A はデータベースからユーザー情報を読み取り、ユーザー ポイントが 1000 であることを確認します

    • ##スレッド A は 100 を加算しますユーザーポイントは 1100 になり、データベースを更新します。

    • スレッド A は 5 秒間スリープします (この時間がデータベースを同期するには十分であると仮定します)

    • スレッド A がキャッシュを再度削除します。

    • #スレッド B のユーザー情報が最初にキャッシュ内のユーザー情報を削除します。
    • スレッド次に、B はデータベースからユーザー情報を読み取り、ユーザーのポイントが 1100 であることを確認します (スレッド A がすでに更新されているため)
    • スレッド B はユーザーのポイントから 50 を引いて 1050、そしてデータベースに更新します
    • スレッド B は 5 秒間スリープします (この時間がデータベースを同期するには十分であると仮定します)
    • スレッド B は削除しますキャッシュ内のユーザー情報を再度確認します。
    • 最終結果は、データベース内のユーザー ポイントは 1050 で、キャッシュにはユーザー情報がありません。次回ユーザー情報がクエリされるときは、データベースから取得してキャッシュに書き込むのではなく、まずキャッシュから読み取られます。これにより、データの一貫性が確保されます。

    遅延二重削除は、同時実行性の高いシナリオ、特にデータ変更操作が頻繁でクエリ操作が少ない状況に適しています。これにより、データベースへの負荷が軽減され、データの最終的な一貫性が保証されながらパフォーマンスが向上します。遅延二重削除は、データベースのマスターとスレーブの同期に遅延があるシナリオにも適しています。これは、データベースの更新後、スレーブ データベースからの同期が完了する前に、古いキャッシュ データを読み取ってキャッシュに書き戻すことを回避できるためです。

    注: このスリープ時間 = ビジネス ロジック データの読み取りにかかる時間は数百ミリ秒です。読み取りリクエストを確実に終了させるために、書き込みリクエストは、読み取りリクエストによってもたらされる可能性のあるキャッシュされたダーティ データを削除できます。

    以上がRedis と MySQL の間の二重書き込みの整合性の問題を解決する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

    声明:
    この記事はyisu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。