純粋に技術的な乾物共有: 分散トランザクション処理方法の概要

純粋に技術的な乾物共有: 分散トランザクション処理方法の概要

プロジェクト開発では、分散トランザクションを処理する必要があることがよくあります。たとえば、データベースを複数のデータベースに分割すると、元々 1 つのデータベースで実行されていた操作が複数のデータベースにまたがる場合があります。システムがサービスに分割されると、1 つのシステム上の元の操作が複数のシステムにまたがる可能性があります。よく使用するキャッシュ (redis、memcache など) でも、分散トランザクションが関係する場合があります。キャッシュとデータベースは 2 つの異なるエンティティであるため、キャッシュとデータベース間のデータの一貫性をどのように確保するかも重要な考慮事項です。分散トランザクションとは、トランザクションによって処理されるリソースが分散システム内の異なるノードに配置されているトランザクションです。

スタンドアロン システムの場合、通常はデータベースを使用してローカル トランザクションを実装します。たとえば、次の JDBC コードはトランザクションを実装します。

  1. 接続 con = datasource.getConnection();
  2. con.setAutoCommit( false );
  3. ...
  4. CRUD操作を実行すると複数のテーブルが関係する場合があります
  5. ...
  6. con.commit()/con.rollback()

分散システムでは複数のシステムで同じデータベースリンクを共有できないため、上記の処理方法をそのまま使用して分散トランザクションを実装することはできません。

以下では、私が実際の開発で使用した分散トランザクションの扱い方をいくつか紹介し、最後に分散トランザクションの関連理論を紹介してまとめます。

分散トランザクションを避ける

分散トランザクションは処理がより困難であるため、可能な限り回避する必要があります。例えば、顧客情報システムの場合、登録ユーザー数が多いため保存されるデータの量が多すぎるため、別々のデータベースとテーブルに保存されます。顧客情報モデルは、顧客基本情報テーブル、顧客ログインアカウントテーブル、顧客ログインパスワードテーブル、顧客連絡先情報テーブルなど、データベース内の複数のテーブルに対応する複数のサブモデルに分かれています。ログインアカウントテーブルと顧客基本情報テーブルの関係は次のようになっていると仮定します。

user_id と login_id は、それぞれ 2 つのテーブルの主キーです。 user_id は、2 つのテーブルを関連付ける login_info テーブルの外部キーとしても機能します。 user_id と login_id の値は、ユーザーが登録すると自動的に生成されます。 user_info テーブルと login_info テーブルは、それぞれ user_id と login_id を使用して、データベースとテーブルのシャーディング ルールを計算します。各モデルを 10 個のライブラリと 100 個のテーブルに保存するとします。つまり、user_info_00 から user_info_99 までの 100 個のテーブルがあり、そのうち user_info_00 から user_info_09 は最初のライブラリに属し、user_info_10 から user_info_19 は 2 番目のライブラリに属し、以下同様に続きます。

データベースとテーブルを分割した後、user_id と login_id の生成ルール (たとえば、数値文字列をランダムに生成するか、単純に増加するシーケンスを使用するか) を慎重に考慮しないと、同じユーザーの user_info 情報と login_info 情報が 2 つの異なるデータベースに保存され、分散トランザクションが発生する可能性があります。

この問題に直面した場合、最善の解決策は、分散トランザクションの発生を回避する方法を検討することです。ユーザーに関連するすべてのモデル データを 1 つのデータベースに保存する方法を見つければ、分散トランザクションを回避できます。各モデルデータのサブライブラリとサブテーブルのルーティングルールは、各テーブルの主キーID(user_id、login_idなど)によって決定されるため、各テーブルの主キー生成ルールをカスタマイズすれば、ユーザーのすべてのモデルデータが同じライブラリに格納されることが保証されます。次の ID 生成ルールを想定します。

  • 最初の 2 桁はモデル識別番号です。たとえば、user_id は 01 で始まり、login_id は 02 で始まります。
  • 次の 11 ビットはシーケンス番号であり、増分シーケンス番号です。より多くの ID が必要な場合は、この部分のビット数を増やすことができますが、ユーザー情報を保存するには 11 ビットで十分です。
  • 次のステップは、データベースとテーブルを分割することです。各モデルのデータベースとテーブル分割アルゴリズムが同じであれば、各モデルの主キー ID のデータベースとテーブル分割位置が同じであれば、ユーザーのすべてのモデルデータが同じデータベースに格納されることが保証されます。
  • 最後の桁は ID チェック ディジットであり、ID の検証を容易にするために、前の 15 桁の内容に基づいて生成されます。

このアイデアに基づいて、ユーザーが登録するときに user_id を生成し、user_id のサブライブラリとサブテーブルの場所をランダムに生成することができます。次に、他のモデル (login_id など) の主キー ID を生成するときに、このモデルの主キー ID のサブライブラリとサブテーブルの位置が user_id と同じである必要があります。注意すべきもう 1 つの点は、テーブルのクエリ条件が必ずしも主キー ID に限定される必要はないということです。他のクエリ条件列がある場合は、その列の生成ルールにも同じサブライブラリとサブテーブルの位置が含まれていることを確認する必要があります。そうしないと、その列をクエリに使用できません。

この方法では、ユーザーのすべてのモデル データが同じライブラリに保存されることが保証され、分散トランザクションの発生を効果的に回避できます。

取引補償

通常、高い同時実行性に対処する主な方法の 1 つは、分散キャッシュ (Redis など) を追加してクエリ パフォーマンスを向上させることです。分散キャッシュを追加した後のシステムによるデータのクエリのプロセスは次のとおりです。

つまり、まずキャッシュからデータをクエリしてみます。キャッシュがヒットした場合は、結果を直接返します。それ以外の場合は、DB からデータをクエリしてみてください。クエリ DB がヒットすると、データがキャッシュに追加され、次回クエリが実行されたときにキャッシュにヒットできるようになります。

データを更新する場合、通常は最初に DB のデータが更新され、DB への書き込みが成功した後にキャッシュのデータが更新されます。では、キャッシュと DB 間のデータの一貫性をどのように確保するかという疑問が生じます。キャッシュと DB は 2 つの異なるエンティティであるため、DB が正常に書き込まれた後にキャッシュが更新されます。キャッシュの更新が失敗した場合 (たとえば、ネットワーク ジッタによりキャッシュが一時的に利用できなくなる場合)、キャッシュと DB に不整合が発生します。このとき、上図のクエリ ロジックに従って、最初にキャッシュを検索すると、「ダーティ」データがクエリされ、ビジネスに重大な影響を及ぼします。これも典型的な分散トランザクションの問題です。キャッシュと DB は同時に正常に更新されるか、同時に更新に失敗します。この問題を解決するより良い方法は、トランザクション補償です。

DB にトランザクション補償テーブル transaction_log を作成できます。 transaction_log テーブルは、ビジネス データと同じデータベースに配置することも、別のデータベースに配置することもできます。データを更新する前に、更新するモデルデータをtransaction_logに記録します。たとえば、user_info テーブルのデータを更新すると、transaction_log に userId が記録されます。

transaction_log レコードが成功すると、ビジネス データ テーブル user_info の内容が更新され、最後にキャッシュ内の userInfo データが更新されます。キャッシュが正常に更新されたら、transaction_log テーブル内の対応するレコードを削除できます。

user_info テーブルが更新された後、ネットワーク ジッタまたはその他の理由によりキャッシュの更新が失敗したとします。この場合、transaction_log テーブル内の対応するレコードは引き続き存在し、トランザクションが完了しなかったことを示します。

アプリケーションは、transaction_log テーブル内のレコードを定期的にスキャンするスケジュールされたタスクを作成します (たとえば、2 秒ごとに 1 回)。条件を満たすレコードが見つかった場合、補正ロジックの実行が試行されます。たとえば、ユーザー情報を更新すると、DB 内の user_info テーブルは正常に更新されますが、キャッシュの更新は失敗します。スケジュールされたタスクは、transaction_log テーブル内の対応するレコードが削除されておらず、通常の待機時間が超過していることを検出したため、キャッシュを DB と整合させようとします (キャッシュ内の対応するデータを削除するか、userId に基づいて DB を再クエリしてキャッシュを補充することができます)。補正タスクが完了したら、transaction_log テーブル内の対応するレコードを削除できます。補正タスクが再度失敗した場合、transaction_log テーブル内のレコードは保持され、次のサイクルで再度実行されるまで待機します。

トランザクション補償により、トランザクションの最終的な一貫性が保証されます。つまり、事故が発生した場合、DB とキャッシュの不整合が発生する時間枠 (たとえば 2 秒) が発生しますが、最終的には両方のデータの整合性が保証されます。スケジュールされたタスク サイクルの設定に関しては、ビジネスの「ダーティ」データに対する感度とシステム負荷を考慮する必要があります。

トランザクションメッセージ

金融システムの場合、ユーザーが正常に登録した後にユーザーのアカウントを自動的に作成する必要があるとします。顧客情報はカスタマーセンターシステムで管理され、顧客アカウント情報は会計センターシステムで管理されます。ユーザーが正常に登録された場合、会計システムに顧客のアカウントが正常に作成されたことを確認する必要があります。これも明らかに分散トランザクションの問題です。

この問題に対処するには、前のセクションで紹介したトランザクション補償メカニズムを使用するのが当然です。ただし、登録と口座開設は同期して完了する必要はなく、ユーザーの登録成功イベントを感知する必要があるのは会計システムだけではありません (たとえば、マーケティング システムもユーザーの登録成功イベントを感知し、ユーザーにクーポンを送信する必要がある場合があります)。そのため、非同期通知にはメッセージ メカニズムを使用する方が適切です。すると、「ユーザーが正常に登録された場合、メッセージは正常に送信される必要がある」という疑問が生じます。

このシナリオに対処するには、トランザクション メッセージを使用できます。ただし、前提条件として、使用する MQ ミドルウェアが Alibaba の RocketMQ などのトランザクション メッセージをサポートしている必要があります。現在、市場に出回っている他の主流の MQ ミドルウェア (Kafka や RabbitMQ など) は、トランザクション メッセージをサポートしていません。

次のシーケンス図は、トランザクション メッセージの実行フローを示しています。

  • 通常のメッセージと比較すると、パブリッシャーがメッセージを送信した後、MQ はメッセージをすぐにサブスクライバーに送信するのではなく、メッセージを永続的に保存するだけです。
  • メッセージを正常に送信すると、パブリッシャーはローカル トランザクションを実行します。たとえば、この例で説明したユーザー登録です。
  • ローカル トランザクションが正常に実行されたかどうかに応じて、パブリッシャーは以前に送信されたメッセージをコミットするかロールバックするかを決定します。ロールバックの場合、MQ は以前に保存されたメッセージを削除します。ここでコミットを送信するとします。
  • MQ は、パブリッシャーから送信されたコミットを受信した後にのみ、サブスクライバーにメッセージを送信します。その後、MQ の信頼性の高いメッセージ送信機能を使用して、上記の例で説明した口座開設操作など、残りのトランザクション操作を完了するようにサブスクライバーに促すことができます。

注意深い友人は、上図のステップ 5 で問題が発生してコミットが失敗した場合、メッセージ発行者とメッセージ サブスクライバー間のトランザクションに不整合が生じることに気付くでしょう。これを防ぐには、MQ タイムアウト コールバック メカニズムを追加します。

次のシーケンス図は、トランザクション メッセージのコミットが失敗した場合の実行フローを示しています。

MQ がパブリッシャーからコミット/ロールバック通知を長時間受信しない場合、MQ はパブリッシャー アプリケーションをコールバックして、ローカル トランザクションが正常に実行されたかどうか、およびコミットまたはロールバック前のメッセージであるかどうかを問い合わせます。パブリッシャーは、ローカル トランザクションが正常に実行されたかどうかを判断するために、対応するコールバックを提供する必要があります。

TCC 2 フェーズ コミット

シナリオによっては、分散トランザクションに複数の参加者が関与し、各参加者が現在の状態に基づいてトランザクションに応答する必要がある場合があります。

電子商取引の Web サイトで、ユーザーが支払い時に複数の支払い方法から選択できるシナリオを想定します。たとえば、合計支払額が 100 元の場合、ユーザーは 10 元をポイントで支払い、90 元を口座残高で支払うことを選択できます。マーケティングシステムはユーザーのポイントを管理し、会計システムはアカウント残高を管理し、注文システムは注文ステータスの管理を担当します。

  • まず、トランザクションを実行する前に、トランザクションのすべての参加者が条件を満たしていることを確認する必要があります。たとえば、ポイント システムでは、ユーザーのポイントが 10 元を超えていることを確認する必要があり、会計システムでは、取引を開始する前にユーザーのアカウント残高が 90 元を超えていることを確認する必要があります。
  • 第二に、トランザクションのアトミック性が満たされる必要があります。ここでのユーザー ポイント、ユーザー残高、注文ステータスはすべて正常に処理されるか、変更されないままである必要があります。

この分散トランザクション シナリオに対処するには、TCC 2 フェーズ コミット方式を使用できます。

TCC は、トランザクション全体を試行とコミット/キャンセルの 2 つのフェーズに分割します。 TCC プロセス全体には、トランザクション イニシエーター、トランザクション 参加者、トランザクション コーディネーターの 3 つの役割があります。上記の注文支払いを例にとると、TCC を使用してトランザクションを処理するプロセスは次のようになります。

  • 最初の段階である試行では、注文システムがプロモーション システムとアカウント システムをそれぞれ呼び出して、ユーザーに十分なポイントとアカウント残高があるかどうかを問い合わせます。リソースの競争を防ぐために、この段階でリソースがロックされます。つまり、マーケティング システムはユーザーの 10 元のポイントをロックし、会計システムはユーザーの 90 元のアカウント残高をロックします。
  • いずれかの参加者が試行フェーズで処理に失敗した場合(たとえば、ユーザーのポイントが 10 元未満、またはユーザーの残高が 90 元未満)、トランザクション イニシエーター(注文システム)はトランザクション調整コンポーネントに通知し、トランザクション調整コンポーネントはすべてのトランザクション参加者に試行フェーズでロックされたリソースをキャンセルするように通知します。
  • すべての参加者が try フェーズ中にプロセスを正常に完了した場合、トランザクション イニシエーターはコーディネータにトランザクションをコミットするように通知し、コーディネータはすべての参加者にトランザクションのコミットを完了するように通知します。この時点で、実際の残高とポイントの減額がシステムによって完了します。ステップ 2.2 では、注文システムでも注文のステータスを更新する必要があることが想定されています。

ただし、このアプローチだけでは一貫性の問題が残ります。たとえば、第 2 フェーズのコミット中にクラッシュやネットワーク ジッターなどの異常な状況が発生した場合、トランザクションは「最終的に一貫性がない」状態になる可能性があります (参加者が try フェーズのみを実行し、第 2 フェーズを実行しなかった場合、または一部の参加者が第 2 フェーズのコミットに成功し、一部の参加者がコミットに失敗した場合)。この状況に対処するには、例外が発生したときにトランザクションを回復できるように、トランザクション ログを追加する必要があります。

トランザクション ログを記録するための信頼性の高いストレージとして DB を使用できます。ログには、トランザクション実行プロセスのコンテキスト、トランザクション実行ステータス、トランザクション参加者などの情報が含まれている必要があります。トランザクション ログは、トランザクション イニシエーターまたはトランザクション コーディネーターによって記録できます。

トランザクション ログは、マスター トランザクション ログとスレーブ トランザクション ログで構成できます。

  • メイン トランザクション ログは、トランザクション イニシエーターの情報とトランザクション実行の全体的なステータスを記録するために使用されます。
  • スレーブ トランザクション ログは、すべてのトランザクション参加者情報と、各参加者が属するスレーブ トランザクションの実行ステータスを記録するために使用されます。メイン トランザクション ログとは 1 対多の関係があります。

トランザクション ログを使用すると、トランザクション ログを定期的にスキャンして、異常に中断されたトランザクションを見つけることができます。トランザクション ログに記録された情報に従って、残りの参加者はコミットまたはキャンセルをプッシュされ、分散トランザクション全体が「最終的な一貫性」を実現できるようになります。

コミット フェーズで例外が発生した場合のトランザクション補正ロジックは次のとおりです。

TCC 2 フェーズ コミットを実装する場合は、次の点に注意する必要があります。

  1. トランザクションの参加者は、第 2 フェーズでコミットが成功するように、試行フェーズで操作が成功していることを確認する必要があります。
  2. 参加者は、コミットおよびキャンセル インターフェイスを実装する際には冪等性を考慮し、繰り返しのコミット/キャンセル要求を正しく処理できる必要があります。
  3. ビジネスの観点からは、2 つのステージ間の中間状態(第 1 ステージは完了しており、第 2 ステージはまだ開始されていない状態)の取り扱いを考慮する必要があります。これは通常、現在の凍結口座残高を表示するなどの特別なテキストを通じて行うことができます。
  4. ステートフル データの場合、複数のトランザクションが同じリソースで操作されるときは、リソースの分離を確保する必要があります。たとえば、口座残高により、異なる取引操作の金額が分離され、互いに影響を及ぼさないことが保証されます。
  5. ネットワーク パケットの損失や障害などの要因により、参加者が第 1 段階の試行要求を受信した後にコミット/キャンセル要求を受信できず、参加者のリソースがロックされ、解放されなくなる可能性があります。この状態はトランザクションの停止と呼ばれます。トランザクションの中断を防ぐために、最初のフェーズの試行が成功した後の最大待機時間を指定できます。最大待機時間を超えると、ロックされたリソースは自動的に解放されます。

要約する

従来のスタンドアロン トランザクションは、A (原子性)、C (一貫性)、I (独立性)、D (永続性) の 4 つの特性を満たす必要があり、固定されたトランザクションです。分散システムには複数のノードがあるため、4 つの ACID 仕様を完全に満たすことは非常に困難です。そこで、柔軟なトランザクション BASE 理論 (基本的な可用性、ソフト ステート、最終的な一貫性) が誕生しました。

スタンドアロン トランザクションと比較すると、分散トランザクションは A と D では厳密に保証されますが、C と I では制限をある程度緩和する必要があります (中間状態データの表示と最終的な一貫性の許可)。

<<:  クラウドコンピューティングの複雑さに対処する方法

>>:  ハイブリッド環境におけるITの可視性を向上

推薦する

Star 5000丨Jumpserver 1.3 がリリースされ、ハイブリッド IT サポートがさらに強化されました

5月3日に、オープンソースの要塞ホスト Jumpserver 1.3 バージョンが正式にリリースされ...

なぜインターネット マーケティングの旅はいつも困難やトラブルに満ちているのでしょうか?

インターネットの普及により、私たちはますますインターネットに依存するようになっています。特にモバイル...

speedykvm - Dallas KVM/SSD/VPS の簡単なレビュー、高性能

昨日、暇だったのでspeedykvmを推奨する記事「推奨:今年のベストKVM/SSD[raid10]...

Baiduのセキュリティリスク警告を削除する方法

9月3日のアップデートで、百度はウェブサイトのセキュリティリスク警告機能を正式に追加しました。ハッカ...

ブログサイトを最適化するにはどうすればいいですか?

インターネット上のブログは、チャットや知識の探求のためのプラットフォームにもなりつつあります。有名な...

Visual Chinaの広報危機について話しましょう!

コアヒント:画像の著作権は中国の画像ビジネスモデルの基盤であり、世界を征服するための武器です。しかし...

オンラインショッピング、グループ購入、ショッピングガイドから始まるクローズドビジネスループの価値と課題

多くの人が「クローズドループは誤った命題だ」と話すとき、多くの本当の業界関係者は「ははは」と反応する...

TDKのウェブサイトを修正した後、1ヶ月で体重が2段階増加しました

ウェブサイトのTDKの3つの要素(タイトル、説明、キーワード)は、できるだけ変更しない方が良い、そう...

SEOは静かに第2段階へ

オンライン マーケティングが成熟するにつれて、SEO テクノロジーも広く使用され、学習されるようにな...

基本に戻って: コンテンツとは何でしょうか?

[はじめに] 昨日はSEOとは何かについてお話ししました。BaiduはBaidu Web Searc...

欧雲:内モンゴルVPSは年間72元から、北京専用サーバーは398元、香港高防御専用サーバー(50G防御)は498元

Ouxun Cloud(すべての資格を備えた遼寧省の国内企業)は現在、大規模な秋のプロモーションを実...

現在、Weibo マーケティングの焦点はホットスポットの作成にあります。

ショートビデオ、セルフメディア、インフルエンサーのためのワンストップサービス業界ではよく知られている...

360とキングソフトの間で火薬のない戦争が再び始まろうとしている

今朝、コンピューターの電源を入れたとき、Kingsoft Internet Security が W...

IaaS 向け初のクエリ言語「ZStack クエリ言語 (ZQL)」を発表

UI 作業を簡素化し、運用および保守担当者により柔軟なリソース クエリ方法を提供するために、ZSta...

JD.com の一般二次分類ページの SEO に関する簡単な分析 (パート 3)

昨日、「JD.com の一般的なセカンダリ カテゴリ ページの SEO に関する簡単な分析 (パート...