クライアントサーバー同期パターン/アルゴリズム?


224

クライアントとサーバーの同期パターンがそこにあるに違いないと感じています。しかし、私は完全に1つをグーグルに失敗しました。

状況は非常に単純です。サーバーは中央ノードであり、複数のクライアントが同じデータに接続して操作します。データはアトムに分割できます。競合が発生した場合、サーバー上にあるものは何でも優先されます(ユーザーが競合を解決しないようにするため)。潜在的に大量のデータがあるため、部分同期が推奨されます。

そのような状況のパターン/良い習慣はありますか、またはあなたがそれを知らない場合-あなたのアプローチは何ですか?

以下は、私が今それを解決しようと考えている方法です。データと並行して、すべてのトランザクションにタイムスタンプが付けられた変更ジャーナルが保持されます。クライアントが接続すると、最後のチェック以降のすべての変更が統合された形式で受信されます(サーバーはリストを調べて追加を削除し、その後に削除、各アトムの更新のマージなど)。Et voila、私たちは最新です。

代わりに、各レコードの変更日を保持し、データの削除を実行する代わりに、削除済みとしてマークするだけです。

何かご意見は?


27
この種のことについては、パターンの話はほとんどないことに同意しました...このシナリオはかなり一般的ですが
Jack Ukleja

回答:


88

分散変更管理のしくみを確認する必要があります。SVN、CVS、および差分作業を管理するその他のリポジトリーを見てください。

いくつかのユースケースがあります。

  • 変更を同期します。変更ログ(またはデルタ履歴)のアプローチはこれに適しています。クライアントはサーバーにデルタを送信します。サーバーは、デルタを統合してクライアントに配布します。これは典型的なケースです。データベースでは、これを「トランザクションレプリケーション」と呼んでいます。

  • クライアントは同期を失いました。バックアップ/復元を使用するか、バグが原因です。この場合、クライアントはデルタを経由せずにサーバーから現在の状態を取得する必要があります。これは、マスターからディテールへのコピーであり、デルタとパフォーマンスは非難されます。それは一度きりのものです。クライアントが壊れています。これを最適化しようとせず、信頼できるコピーを実装してください。

  • クライアントは不審です。この場合、クライアントとサーバーを比較して、クライアントが最新であり、デルタが必要かどうかを判断する必要があります。

すべての変更に連番を付けるというデータベース(およびSVN)の設計パターンに従う必要があります。このようにして、クライアントは同期を試みる前に簡単な要求(「どのリビジョンが必要ですか?」)を行うことができます。それでも、クエリ(「2149以降のすべてのデルタ」)は、クライアントとサーバーが処理するのが非常に簡単です。


デルタとは何ですか?私の推測では、これはハッシュ/タイムスタンプの組み合わせだと思います... u先生からお聞きしたいのですが。
アニス

デルタとは、2つのリビジョン間の変更を指します。たとえば、ユーザーの名前が変更された場合、デルタは{revision:123、name: "John Doe"}
dipole_moment

31

チームの一員として、私はデータの同期を伴う多くのプロジェクトを行ったので、この質問に答える能力が必要です。

データの同期は非常に広い概念であり、議論する余りにも多くがあります。さまざまなアプローチの利点と欠点をカバーしています。これは、同期/非同期、クライアント/サーバー/ピアツーピアの2つの視点に基づく可能な分類の1つです。同期の実装は、これらの要因、データモデルの複雑さ、転送および保存されるデータの量、その他の要件に大きく依存します。したがって、それぞれの特定のケースでは、アプリの要件を満たす最も単純な実装を選択する必要があります。

既存の既製のソリューションのレビューに基づいて、同期の対象となるオブジェクトの粒度が異なるいくつかの主要な同期クラスを描くことができます。

  • ドキュメント全体またはデータベース全体の同期は、Dropbox、Google Drive、Yandex.Diskなどのクラウドベースのアプリケーションで使用されます。ユーザーがファイルを編集して保存すると、新しいファイルバージョンが完全にクラウドにアップロードされ、以前のコピーが上書きされます。競合が発生した場合は、両方のファイルバージョンが保存されるため、ユーザーはより関連性の高いバージョンを選択できます。
  • キーと値のペアの同期は、変数がアトミックであると見なされる、つまり論理コンポーネントに分割されていない、単純なデータ構造のアプリで使用できます。値とドキュメントの両方を完全に上書きできるため、このオプションはドキュメント全体の同期と似ています。ただし、ユーザーの観点から見ると、ドキュメントは多くの部分で構成される複雑なオブジェクトですが、キーと値のペアは短い文字列または数値にすぎません。したがって、この場合、変更が最後である場合は、より関連性の高い値を考慮して、競合解決のより単純な戦略を使用できます。
  • ツリーまたはグラフとして構造化されたデータの同期は、更新のたびにデータベース全体を送信するのに十分な量のデータ量がある、より洗練されたアプリケーションで使用されます。この場合、個々のオブジェクト、フィールド、または関係のレベルで競合を解決する必要があります。私たちは主にこのオプションに焦点を当てています。

そのため、この記事に知識を取り入れ、トピックに興味があるすべての人に非常に役立つと思います=> Core Data Based iOSアプリでのデータ同期(http://blog.denivip.ru/index.php/2014/04) / data-syncing-in-core-data-based-ios-apps /?lang = en


3
^^^^^^これは間違いなく最高の答えです。
hgoebl 2017

私は同意します、デニスはトピックに多くをもたらしました+記事のリンクは素晴らしいです。DanielPaullによって言及されたOTについても話します。S.Lottによる回答は良いですが、これははるかに深いです。
クリスティアン

28

本当に必要なのは、運用変換(OT)です。これは、多くの場合、衝突にも対応できます。

これはまだ活発な研究分野ですが、さまざまなOTアルゴリズムの実装があります。私はこのような調査に長年携わってきましたので、このルートがあなたに興味があるかどうかをお知らせください。関連するリソースを紹介させていただきます。


7
ダニエル、関連リソースへのポインタをいただければ幸いです。
パランド

4
ウィキペディアの記事をもう一度読みました。それは長い道のりであり、そのページの下部に多くの関連参照があります。Chengzheng Sunの作品を紹介しました-彼の作品はウィキペディアから参照されています。en.wikipedia.org/wiki/Operational_transformation。お役に立てば幸いです。
Daniel Paull

13

問題は明確ではありませんが、私があなただったら楽観的ロックについて調べたいと思います。サーバーが各レコードに対して返すシーケンス番号を使用して実装できます。クライアントがレコードを保存しようとすると、サーバーから受信したシーケンス番号が含まれます。シーケンス番号が更新の受信時にデータベースにあるものと一致する場合、更新が許可され、シーケンス番号が増分されます。シーケンス番号が一致しない場合、更新は許可されません。


2
シーケンス番号はあなたの友達です。永続的なメッセージキューについて考えます。
Daniel

7

私はこのようなシステムを約8年前にアプリ用に構築しましたが、アプリの使用が増えるにつれて進化する方法をいくつか共有できます。

まず、任意のデバイスからのすべての変更(挿入、更新、または削除)を「履歴」テーブルに記録しました。たとえば、誰かが「連絡先」テーブルの電話番号を変更した場合、システムはcontact.phoneフィールドを編集し、action = update、field = phone、record = [contact ID]、 value = [新しい電話番号]。その後、デバイスが同期するたびに、前回の同期以降の履歴アイテムがダウンロードされ、ローカルデータベースに適用されます。これは、上記の「トランザクションレプリケーション」パターンのように聞こえます。

1つの問題は、異なるデバイスでアイテムを作成できる場合にIDを一意に保つことです。これを始めたときはUUIDについて知らなかったので、自動インクリメントIDを使用し、中央サーバーで実行される複雑なコードを記述して、デバイスからアップロードされた新しいIDを確認し、競合がある場合は一意のIDに変更しました。ローカルデータベースのIDを変更するようにソースデバイスに指示します。新しいレコードのIDを変更するだけでそれほど問題はありませんでしたが、たとえば、連絡先テーブルに新しいアイテムを作成し、イベントテーブルに新しい関連アイテムを作成すると、外部キーも必要になります。チェックして更新します。

最終的に私はUUIDでこれを回避できることを学びましたが、それまでにデータベースがかなり大きくなり、完全なUUID実装がパフォーマンスの問題を引き起こすと心配しました。そのため、完全なUUIDを使用する代わりに、ランダムに生成された8文字の英数字キーをIDとして使用し始め、既存のコードを残して競合を処理しました。私の現在の8文字のキーとUUIDの36文字の間のどこかに、不必要な膨張なしに競合を解消するスイートスポットがあるはずですが、私はすでに競合解決コードを持っているので、それを試すことは優先事項ではありませんでした。

次の問題は、履歴テーブルが残りのデータベース全体の約10倍であったことです。これにより、ストレージが高価になり、履歴テーブルのメンテナンスが困難になる可能性があります。そのテーブル全体を維持することで、ユーザーは以前の変更をロールバックできますが、それはやり過ぎのように感じ始めました。そこで、デバイスが最後にダウンロードした履歴アイテムが履歴テーブルに存在しなくなった場合、サーバーは最近の履歴アイテムを提供せず、代わりにすべてのデータを含むファイルを提供するルーチンを同期プロセスに追加しましたそのアカウント。次に、cronジョブを追加して、90日以上経過した履歴アイテムを削除しました。つまり、ユーザーは90日未満の変更を引き続きロールバックでき、90日ごとに少なくとも1回同期すると、更新は以前と同様に増分されます。でも90日以上待つと

この変更により、履歴テーブルのサイズがほぼ90%減少したため、履歴テーブルを維持しても、データベースのサイズは10倍ではなく2倍になりました。このシステムのもう1つの利点は、必要に応じて履歴テーブルがなくても同期が機能することです(一時的にオフラインにするメンテナンスを行う必要がある場合など)。または、異なる価格でアカウントに異なるロールバック期間を提供することもできます。また、ダウンロードする変更が90日以上ある場合は、通常、完全なファイルの方が増分形式よりも効率的です。

今日からやり直した場合は、IDの競合チェックをスキップし、念のため、何らかのエラーチェックを行って、競合を排除するのに十分なキーの長さを目指します。しかし、履歴テーブルと、最近の更新の増分ダウンロードの組み合わせ、または必要な場合の完全ダウンロードはうまく機能しています。


1

差分(変更)同期の場合、pubsubパターンを使用して、サブスクライブしているすべてのクライアント、プッシャーなどのサービスに変更を公開できます。できます。これを行うことができます。

データベースミラーの場合、一部のWebフレームワークはローカルミニデータベースを使用してサーバー側のデータベースをブラウザーデータベースのローカルに同期します。部分的な同期がサポートされています。メーターを確認してください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.