MongoDBでのトランザクションの不足を回避するにはどうすればよいですか?


139

同様の質問がここにあることは知っていますが、トランザクションが必要な場合、またはアトミック操作または2フェーズコミットを使用する場合は、通常のRDBMSシステムに切り替えるように指示しています。2番目のソリューションが最良の選択のようです。3つ目は、多くの問題が発生する可能性があり、あらゆる面でテストすることができないため、フォローしたくありません。アトミック操作を実行するためにプロジェクトをリファクタリングするのに苦労しています。これが私の限られた視点から来たのか(これまでSQLデータベースでしか作業していない)のか、それとも実際に実行できないのかはわかりません。

弊社でMongoDBをパイロットテストしたいと思います。私たちは比較的単純なプロジェクト、つまりSMSゲートウェイを選択しました。私たちのソフトウェアがSMSメッセージをセルラーネットワークに送信することを可能にし、ゲートウェイは、実際にはさまざまな通信プロトコルを介してプロバイダーと通信するという汚い仕事をします。ゲートウェイは、メッセージの請求も管理します。サービスを申請するすべての顧客は、いくつかのクレジットを購入する必要があります。システムは、メッセージが送信されると自動的にユーザーの残高を減らし、残高が不足している場合はアクセスを拒否します。また、私たちはサードパーティのSMSプロバイダーのお客様であるため、独自の残高を提供する場合もあります。それらも追跡する必要があります。

複雑さを軽減する場合(外部請求、キューSMS送信)、MongoDBで必要なデータを保存する方法について考え始めました。SQLの世界から来て、ユーザー用の別のテーブル、SMSメッセージ用の別のテーブル、およびユーザーのバランスに関するトランザクションを格納するためのテーブルを作成します。MongoDBのすべてのコレクションを個別に作成するとします。

この簡略化されたシステムで次の手順を実行するSMS送信タスクを想像してください。

  1. ユーザーに十分なバランスがあるかどうかを確認します。十分なクレジットがない場合はアクセスを拒否します

  2. 詳細とコストを含むSMSコレクションにメッセージを送信して保存します(ライブシステムではメッセージにstatus属性があり、タスクは配信のためにそれを取得し、現在の状態に応じてSMSの価格を設定します)

  3. 送信されたメッセージのコストによってユーザーのバランスを減らす

  4. トランザクションをトランザクションコレクションに記録する

それで問題は何ですか?MongoDBは、1つのドキュメントに対してのみアトミック更新を実行できます。前のフローでは、なんらかのエラーが発生し、メッセージがデータベースに格納されても、ユーザーの残高が更新されなかったり、トランザクションがログに記録されなかったりする場合があります。

私は2つのアイデアを思いつきました:

  • ユーザーの単一のコレクションを作成し、残高をフィールドとして保存し、ユーザー関連のトランザクションとメッセージをユーザーのドキュメントのサブドキュメントとして保存します。ドキュメントをアトミックに更新できるため、実際にはトランザクションの問題が解決されます。短所:ユーザーが多数のSMSメッセージを送信すると、ドキュメントのサイズが大きくなり、4MBのドキュメント制限に達する可能性があります。そのようなシナリオで履歴ドキュメントを作成できるかもしれませんが、これは良い考えではないと思います。また、同じ大きなドキュメントにさらに多くのデータをプッシュすると、システムがどのくらい高速になるかわかりません。

  • ユーザー用とトランザクション用に1つのコレクションを作成します。トランザクションに、正の残高変更のあるクレジット購入と負の残高変更のある送信メッセージの 2種類があります。トランザクションにはサブドキュメントがある場合があります。たとえば、送信されメッセージでは、SMSの詳細をトランザクションに埋め込むことができます。欠点:現在のユーザーの残高は保存しないので、ユーザーがメッセージを送信しようとするたびに計算して、メッセージが通過できるかどうかを判断する必要があります。保存されたトランザクションの数が増えると、この計算が遅くなる可能性があります。

どの方法を選択するかについて少し混乱しています。他の解決策はありますか?これらの種類の問題を回避する方法に関するオンラインのベストプラクティスを見つけることができませんでした。NoSQLの世界に慣れようとするプログラマーの多くは、最初は同様の問題に直面していると思います。


61
私が間違っている場合は許してくださいが、このプロジェクトが恩恵を受けるかどうかに関係なく、このプロジェクトはNoSQLデータストアを使用するように見えます。NoSQLは「ファッション」の選択肢としてのSQLの代替ではありませんが、リレーショナルRDBMSのテクノロジーが問題の領域に適合せず、非リレーショナルデータストアが適合する場合に使用します。あなたの質問の多くは「もしそれがSQLだったら…」でした、そしてそれは私に警告の鐘を鳴らします。NoSQLはすべて、SQLで解決できなかった問題を解決する必要性から生まれたもので、使いやすくするために多少一般化されており、当然ながらバンドワゴンが動き始めています。
PurplePilot 2011

4
このプロジェクトはNoSQLを試すのに最適ではないことを知っています。しかし、他のプロジェクト(コレクション管理をしているので、ライブラリコレクション管理ソフトウェアと言いましょう)でそれを使い始めて、突然、トランザクションを必要とするある種の要求が来たら(そして実際にはそこにあり、本があるコレクションから別のコレクションに転送されます)問題を克服する方法を知る必要があります。たぶん私だけが狭い心を持っており、常に取引の必要性があると思っています。しかし、これらをどうにかして克服する方法があるかもしれません。
NagyI 2011

3
PurplePilotに同意します。問題に適していないソリューションを移植しようとするのではなく、ソリューションに適合するテクノロジーを選択する必要があります。グラフデータベースのデータのモデリングは、RDBMSの設計とはまったく異なるパラダイムであり、知っていることをすべて忘れて、新しい考え方を再学習する必要があります。

9
タスクに適切なツールを使用する必要があることは理解しています。しかし私にとって-私がこのような答えを読んだとき-NoSQLはデータが重要であるものには何も良いようではないようです。FacebookやTwitterに適しています。コメントが失われた場合、世界は続きますが、それ以上のことはすべて廃業します。それが本当なら、なぜ他の人がビルドを気にするのか分かりません。MongoDBを備えたウェブストア:kylebanker.com/blog/2010/04/30/mongodb-and-ecommerceさらに、ほとんどのトランザクションはアトミック操作で克服できると述べています。私が探しているのはその方法です。
NagyI 2011

2
トランザクションACIDタイプのトランザクション処理が適切でない(たぶん)場合、「NoSQLは、データが重要な場所には適していないと思われます」とは言えません。また、NoSQLは、マスタースレーブレプリケーションシナリオに入るときにSQLタイプのストアを実現するのが非常に難しい分散データストア用に設計されています。NoSQLには結果整合性のための戦略があり、最新のデータセットのみが使用され、ACIDは使用されないようにします。
PurplePilot

回答:


23

4.0以降、MongoDBにはマルチドキュメントACIDトランザクションがあります。計画では、レプリカセットのデプロイメントでそれらを最初に有効にし、その後、シャーディングされたクラスターを有効にします。MongoDBのトランザクションは、リレーショナルデータベースの開発者が使い慣れているトランザクションと同じように感じられます。トランザクションは、同様のセマンティクスと構文(start_transactionおよびなどcommit_transaction)を持つマルチステートメントになります。重要なことに、トランザクションを可能にするMongoDBへの変更は、トランザクションを必要としないワークロードのパフォーマンスに影響を与えません。

詳細はこちらをご覧ください。

トランザクションを分散しても、表形式のリレーショナルデータベースのようにデータをモデル化する必要があるという意味ではありません。ドキュメントモデルの力を活用し、データモデリングの推奨される推奨プラクティスに従ってください。


1
トランザクションが到着しました!4.0 GAeded。mongodb.com/blog/post/...
グリゴリーメルニーク

MongoDBトランザクションにはまだトランザクションのサイズに16 MBの制限があります。最近、ファイルから50kレコードをmongoDBに入れる必要があるユースケースがありました。そのため、アトミックプロパティを維持するために、トランザクションの使用を考えましたが、50k jsonレコードこの制限を超えると、「すべてのトランザクション操作の合計サイズは16793600未満でなければなりません。実際のサイズは16793817です」というエラーがスローされます。詳細については、mongoDB jira.mongodb.org/browse/SERVER-36330で
Gautam Malik

MongoDB 4.2(現在ベータ版、RC4)は大規模なトランザクションをサポートしています。複数のoplogエントリにまたがるトランザクションを表すことにより、1つのACIDトランザクションで16MBを超えるデータを書き込むことができます(既存の60秒のデフォルトの最大実行時間に従います)。今すぐ試すことができます-mongodb.com/download-center/community
Grigori Melnik

MongoDB 4.2がGAになり、分散トランザクションが完全にサポートされるようになりました。mongodb.com/blog/post/...
グリゴリーメルニーク

83

トランザクションなしで生きる

トランザクションはACIDプロパティをサポートしますが、にはトランザクションはMongoDBありませんが、アトミック操作があります。まあ、アトミック操作とは、単一のドキュメントで作業する場合、その作業は他の誰かがそのドキュメントを見る前に完了することを意味します。彼らは私たちが行ったすべての変更を見るか、どれも見ないでしょう。また、アトミック操作を使用すると、リレーショナルデータベースでトランザクションを使用して達成したのと同じことを達成できることがよくあります。その理由は、リレーショナルデータベースでは、複数のテーブルにわたって変更を加える必要があるためです。通常、結合する必要のあるテーブルなので、一度にすべて結合したいと考えています。そしてそれを行うには、複数のテーブルがあるため、トランザクションを開始し、それらのすべての更新を実行してから、トランザクションを終了する必要があります。しかし、MongoDB、それをドキュメントに事前結合するので、データを埋め込みます。これらは階層を持つこれらのリッチドキュメントです。多くの場合、同じことを達成できます。たとえば、ブログの例で、ブログ投稿をアトミックに更新したい場合は、ブログ投稿全体を一度に更新できるため、これを実行できます。リレーショナルテーブルの束であるかのように、投稿コレクションとコメントコレクションを更新できるように、おそらくトランザクションを開く必要があります。

ではMongoDB、トランザクションの不足を克服するために私たちが取り入れることができる私たちのアプローチは何ですか?

  • 再構成 -コードを再構成して、単一の文書内で作業し、その文書内で提供するアトミック操作を利用できるようにします。そして、それを行えば、通常はすべて準備が整います。
  • ソフトウェアで実装 -重要なセクションを作成することにより、ソフトウェアでロックを実装できます。findとmodifyを使用して、テスト、テスト、および設定を構築できます。必要に応じて、セマフォを構築できます。そして、ある意味で、それはとにかくより大きな世界が機能する方法です。考えてみると、ある銀行が別の銀行に送金する必要がある場合、それらは同じリレーショナルシステムに住んでいません。そして、それぞれが独自のリレーショナルデータベースを持っていることがよくあります。そして、それらのデータベースシステム全体でトランザクションを開始および終了できなくても、1つのバンク内の1つのシステム内でのみ、それらの操作を調整できる必要があります。したがって、ソフトウェアで問題を回避する方法は確かにあります。
  • 許容 -大量のデータを取り込む最新のWebアプリやその他のアプリケーションでしばしば機能する最終的なアプローチは、多少の不整合を許容することです。たとえば、Facebookで友達のフィードについて話している場合、壁の更新がすべての人に同時に表示されるかどうかは関係ありません。大丈夫なら、一人の人が数秒間遅れて数秒遅れて追いついたら。多くのシステム設計では、すべてが完全に一貫していること、およびすべての人が完全に一貫していて同じデータベースのビューを持っていることは、多くの場合重要ではありません。そのため、一時的な一時的な不整合を許容することができます。

UpdatefindAndModify$addToSet(更新中)& $push(更新中)の動作は、単一の文書内アトミックに動作します。


2
リレーショナルDBに戻る必要があるかどうか質問するのではなく、この回答の方法が気に入っています。ありがとう@xameeramir!
DonnyTian 2017

3
複数のサーバーがあり、外部分散ロックサービスを使用する必要がある場合、コードの重要なセクションは機能しません
Alexander Mills

@AlexanderMills詳しく説明していただけますか?
Zameer

回答はこちらからの動画のトランスクリプトのようです:youtube.com/watch
Fritz

これは、単一のコレクションでの操作が制限されるまでは問題ないと思われます。しかし、さまざまな理由(ドキュメントのサイズまたは参照を使用している場合)のため、すべてを1つのドキュメントに入れることはできません。それで私達はトランザクションが必要になるかもしれないと思います。
user2488286

24

Tokutekによるこれをチェックてください。トランザクションだけでなくパフォーマンスの向上も約束するMongo用のプラグインを開発しています。


@Giovanniビットライナー。Tokutekはその後Perconaによって買収されており、あなたが与えたリンク上では、投稿以降に起こったことに対する情報への言及は見当たらない。彼らの努力がどうなったのか知っていますか?そのページのメールアドレスをメールで確認しました。
Tyler Collier

具体的に何が必要ですか?Mongodbに適用されたtokuテクノロジーが必要な場合は、github.com/Tokutek/mongoを試してください。mysqlバージョンが必要な場合は、通常提供している
Mysqlの

どうすればtokutekとnodejsを統合できますか?
Manoj Sanjeewa

11

重要なことは、トランザクションの整合性が必須の場合は、MongoDBを使用せずに、トランザクションをサポートするシステムのコンポーネントのみを使用することです。非ACID準拠のコンポーネントにACIDと同様の機能を提供するために、コンポーネントの上に何かを構築することは非常に困難です。個々のユースケースによっては、何らかの方法でアクションをトランザクションアクションと非トランザクションアクションに分離することが理にかなっています...


1
NoSQLを従来のRDBMSのサイドキックデータベースとして使用できると思います。同じプロジェクトでNoSQLとSQLを混在させるのは好きではありません。それは複雑さを増し、おそらくいくつかの重要な問題も引き起こします。
NagyI 2011

1
NoSQLソリューションが単独で使用されることはほとんどありません。ドキュメントストア(mongoとcouch)は、おそらくこのルールの唯一の例外です。
Karoly Horvath、2011

7

それで問題は何ですか?MongoDBは、1つのドキュメントに対してのみアトミック更新を実行できます。前のフローでは、なんらかのエラーが発生し、メッセージがデータベースに格納されても、ユーザーの残高が減らされなかったり、トランザクションがログに記録されなかったりすることがあります。

これは本当に問題ではありません。あなたが言及したエラーは、論理(バグ)またはIOエラー(ネットワーク、ディスク障害)のいずれかです。この種のエラーにより、トランザクションレスストアとトランザクションストアの両方が一貫性のない状態になる可能性があります。たとえば、すでにSMSを送信したが、メッセージの保存中にエラーが発生した場合-SMS送信をロールバックできません。つまり、ログに記録されず、ユーザーのバランスが低下しません。

ここでの本当の問題は、ユーザーが競合状態を利用して、バランスが許すよりも多くのメッセージを送信できることです。これは、バランスフィールドロックを使用してトランザクション内でSMS送信を実行しない限り(これは大きなボトルネックになる)、RDBMSにも適用されます。MongoDBの可能な解決策として、findAndModify残高を減らして確認するために最初に使用することが考えられます。負の場合は、送信を拒否して金額を払い戻します(アトミック増分)。プラスの場合は送信を続行し、失敗した場合は金額を払い戻します。残高履歴の収集も維持して、残高フィールドの修正/検証に役立てることができます。


この素晴らしい答えをありがとう!トランザクション対応のストレージを使用すると、SMSシステムが原因で制御できないデータが破損する可能性があることは知っています。ただし、Mongoでは社内でデータエラーが発生する可能性もあります。たとえば、コードがfindAndModifyでユーザーの残高を変更すると、残高がマイナスになりますが、間違いを修正する前にエラーが発生し、アプリケーションを再起動する必要があります。トランザクションコレクションに基づいて2フェーズコミットに似たものを実装し、データベースで定期的に修正チェックを行うべきだと思います。
NagyI 2011

9
真実ではありません。トランザクションストアは、最終的なコミットを行わないとロールバックされます。
Karoly Horvath、2011

9
また、SMSを送信してからDBにログインすることはありません。最初にすべてをDBに保存し、最後のコミットを実行してから、メッセージを送信できます。この時点でまだ何かが失敗する可能性があるため、送信しない場合は、メッセージが実際に送信されたことを確認するcronジョブが必要です。おそらく、専用のメッセージキューがこれに適しています。しかし、全体の事は...あなたはトランザクションの方法でSMSesを送信できるかどうかに沸く
カロリー・ホーバス

@NagyIはい、それは私が意味したことです。スケーラビリティを容易にするために、トランザクションの利点を交換する必要があります。基本的にアプリケーションは、異なるコレクション内の2つのドキュメントが不整合な状態にあり、これを処理する準備ができていることを期待する必要があります。@yi_Hこれはロールバックしますが、状態は実際にはありません(メッセージに関する情報は失われます)。これは、部分的なデータを保持することよりもはるかに優れているわけではありません(残高は減るがメッセージ情報はない、またはその逆など)。
pingw33n

そうですか。これは実際には簡単な制約ではありません。多分私はRDBMSシステムがトランザクションをどのように行うかについてもっと学ぶべきです。これらについて読むことができるある種のオンライン資料または本をお勧めできますか?
NagyI 2011

6

プロジェクトは単純ですが、支払いのトランザクションをサポートする必要があるため、全体が難しくなります。したがって、たとえば、何百ものコレクション(フォーラム、チャット、広告など)を含む複雑なポータルシステムは、フォーラムやチャットのエントリを失っても誰も気にしないので、いくつかの点でより単純です。一方、あなたが深刻な問題である支払い取引を失った場合。

したがって、MongoDBのパイロットプロジェクトが本当に必要な場合は、その点で単純なプロジェクトを選択してください


説明していただきありがとうございます。それを聞いて悲しい。NoSQLのシンプルさとJSONの使用が好きです。私たちはORMの代替を探していますが、しばらくそれを使い続ける必要があるようです。
NagyI

このタスクでMongoDBがSQLよりも優れている理由を教えてください。パイロットプロジェクトは少しばかげています。
Karoly Horvath、2011

MongoDBがSQLより優れているとは言いませんでした。SQL + ORMより優れているかどうかを知りたいだけです。しかし、この種のプロジェクトでは競争力がないことが明らかになりつつあります。
NagyI

6

正当な理由により、MongoDBにはトランザクションがありません。これは、MongoDBを高速化するものの1つです。

あなたの場合、トランザクションが必須である場合、mongoは適切ではないようです。

RDMBS + MongoDBの場合もありますが、これにより複雑さが増し、アプリケーションの管理とサポートが困難になります。


1
:50倍の性能向上を実現するためにフラクタル技術を使用し、同時に完全なACIDトランザクションをサポートできますTokuMXと呼ばれるのMongoDBの分布用意されましたtokutek.com/tokumx-for-mongodbを
OCDev

9
トランザクションが「必須」ではない可能性があるのはなぜですか。2つのテーブルを更新する必要がある1つの単純なケースが必要になるとすぐに、mongoは突然適切ではなくなりますか?それは、非常に多くのユースケースを残しません。
Mr_E、2015年

1
@Mr_E同意、それがMongoDBがちょっとばかげている理由です:)
Alexander Mills

6

これはおそらく、mongodbの機能のようなトランザクションの実装に関して私が見つけた最高のブログです。

同期フラグ:マスタードキュメントからデータをコピーするだけの場合に最適

ジョブキュー:非常に一般的な目的で、95%のケースを解決します。ほとんどのシステムでは、とにかく少なくとも1つのジョブキューが必要です。

2フェーズコミット:この手法により、各エンティティは常に、一貫した状態に到達するために必要なすべての情報を持つことができます。

ログ調整:金融システムに最適な、最も堅牢な手法

バージョン管理:分離を提供し、複雑な構造をサポートします

詳細については、こちらをご覧ください:https : //dzone.com/articles/how-implement-robust-and


質問の回答に必要なリンクされたリソースの関連部分を回答に含めてください。現状のままでは、あなたの回答はリンクの腐敗の影響を非常に受けやすくなっています(つまり、リンクされたWebサイトがダウンしたり、変更を変更したりすると、回答が役に立たなくなる可能性があります)。
メカ

提案をありがとう@mech
Vaibhav

4

これは遅いですが、将来的には役立つと思います。この問題を解決するために、Redisを使用してキューを作成します

  • 要件:
    下の画像は、2つのアクションを同時に実行する必要があるが、アクション1のフェーズ2とフェーズ3は、アクション2の開始フェーズ2またはその反対の前に完了する必要があることを示しています(フェーズは、リクエストREST API、データベースリクエスト、またはJavaScriptコードの実行です... )。 ここに画像の説明を入力してください

  • キューヘルプあなたはどのように
    間のすべてのブロックコードことを確認してキューlock()release()、多くの機能では、同じ時間として実行されませんそれらを分離します。

    function action1() {
      phase1();
      queue.lock("action_domain");
      phase2();
      phase3();
      queue.release("action_domain");
    }
    
    function action2() {
      phase1();
      queue.lock("action_domain");
      phase2();
      queue.release("action_domain");
    }
  • キューを構築する方法
    私は、バックエンドサイトでキューを構築するときに、競合条件部分を回避する方法にのみ焦点を当てます。キューの基本的な考え方がわからない場合は、こちらにアクセスしてください
    以下のコードは概念のみを示しています。正しい方法で実装する必要があります。

    function lock() {
      if(isRunning()) {
        addIsolateCodeToQueue(); //use callback, delegate, function pointer... depend on your language
      } else {
        setStateToRunning();
        pickOneAndExecute();
      }
    }
    
    function release() {
      setStateToRelease();
      pickOneAndExecute();
    }

しかし、あなたはisRunning() setStateToRelease() setStateToRunning()それを自分自身から切り離す必要があります。そうしないと、再び競合状態に直面します。これを行うには、ACIDの目的でスケーラブルなRedisを選択します。
Redisのドキュメントでは、そのトランザクションについて説明しています

トランザクション内のすべてのコマンドはシリアル化され、順次実行されます。Redisトランザクションの実行中に別のクライアントから発行されたリクエストが処理されることは決してありません。これにより、コマンドが単一の分離された操作として実行されることが保証されます。

P / s:
私は自分のサービスが既にそれを使用しているのでRedisを使用しますが、分離をサポートする他の方法を使用できます。私のコードでは、あなたが他のユーザーをブロックしない、ユーザAのユーザAブロックアクション2によってのみ行動1つのコールを必要とするときのために上です。アイデアは、各ユーザーのロックのためのユニークなキーを置くことです。
action_domain


スコアが既に高ければ、より多くの賛成票を受け取ることになります。それがここで最も考えていることです。あなたの答えは質問の文脈で役に立ちます。私はあなたに賛成票を投じました。
ムクス

3

トランザクションはMongoDB 4.0で利用可能になりました。ここにサンプル

// Runs the txnFunc and retries if TransientTransactionError encountered

function runTransactionWithRetry(txnFunc, session) {
    while (true) {
        try {
            txnFunc(session);  // performs transaction
            break;
        } catch (error) {
            // If transient error, retry the whole transaction
            if ( error.hasOwnProperty("errorLabels") && error.errorLabels.includes("TransientTransactionError")  ) {
                print("TransientTransactionError, retrying transaction ...");
                continue;
            } else {
                throw error;
            }
        }
    }
}

// Retries commit if UnknownTransactionCommitResult encountered

function commitWithRetry(session) {
    while (true) {
        try {
            session.commitTransaction(); // Uses write concern set at transaction start.
            print("Transaction committed.");
            break;
        } catch (error) {
            // Can retry commit
            if (error.hasOwnProperty("errorLabels") && error.errorLabels.includes("UnknownTransactionCommitResult") ) {
                print("UnknownTransactionCommitResult, retrying commit operation ...");
                continue;
            } else {
                print("Error during commit ...");
                throw error;
            }
       }
    }
}

// Updates two collections in a transactions

function updateEmployeeInfo(session) {
    employeesCollection = session.getDatabase("hr").employees;
    eventsCollection = session.getDatabase("reporting").events;

    session.startTransaction( { readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } } );

    try{
        employeesCollection.updateOne( { employee: 3 }, { $set: { status: "Inactive" } } );
        eventsCollection.insertOne( { employee: 3, status: { new: "Inactive", old: "Active" } } );
    } catch (error) {
        print("Caught exception during transaction, aborting.");
        session.abortTransaction();
        throw error;
    }

    commitWithRetry(session);
}

// Start a session.
session = db.getMongo().startSession( { mode: "primary" } );

try{
   runTransactionWithRetry(updateEmployeeInfo, session);
} catch (error) {
   // Do something with error
} finally {
   session.endSession();
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.