Column-Level Conflict Detection

デフォルトでは、競合行レベルで解決されます。つまり、2つのノードからの変更が競合する場合、ローカルまたはリモートのタプルのいずれかを選択し、もう一方を破棄します。例、2つの競合する変更のコミットタイムスタンプを比較し、新しいものを保持する場合があります。これにより、すべてのノードが同じ結果に収束することが保証され、クラスター全体でコミット順序のようなセマンティクスが確立されます。

ただし、場合によっては、行レベルレベルではなく列レベルで競合を解決することが適切な場合があります。

2つの整数列 “a”と “b”を持つテーブル “t”と単行(1,1)があるシンプル例を考えてみましょう。 1つのノードで実行と仮定します。

UPDATE t SET a = 100

…別のノードで同時に(precedUPDATEを受信する前に)実行ます:

UPDATE t SET b = 100

これにより、UPDATE-UPDATEの競合が発生します。 update_if_newerconflict解決では、コミットタイムスタンプを比較し、newrowバージョンを保持します。 2番目のノードが最後にコミットされたと仮定すると、(1,100)になり、列「a」への変更を事実上破棄します。

多くのユースケースでは、これは望ましい動作であり期待される動作ですが、場合によってはこれが問題になる可能性があります-例、アプリケーションの各パートが異なるノードに接続され、共有テーブルの列の専用サブセットを更新するマルチノードクラスターを検討してください。その場合、異なるコンポーネントが互いのつま先を踏んで、変更を上書きする可能性があります。

このようなユースケースでは、列レベルで特定のテーブルの競合を解決する方が適切な場合があります。これを実現するために、 BDRは各列の最後の変更のタイムスタンプを個別に追跡し、それを使用して最新の値(本質的にupdate_if_newer)を選択します。

前の例に適用すると、両方のノードで(100,100)になりますが、どちらのノードにもこのような行は表示されません。

列レベルの競合解決を検討する場合、各更新が1つのスライス内のデータのみに影響するように、テーブルが垂直にパーティション化されているように見えると便利な場合があります。これにより、列の異なるサブセットに対する変更間の競合がなくなります。実際、垂直パーティショニングは列レベルの競合解決の実用的な代替手段でさえあります。

列レベルの競合解決には、テーブルにREPLICAIDENTITY FULLが必要です。 bdr.alter_table_conflict_detectionファンクションはそれをチェックし、そうでなければエラーで失敗します。

!!! Note * 現在、この機能はEDB Postgres ExtendedおよびEDB Postgres Advancedでのみ利用可能です。

列レベルの競合解決の有効化と無効化

列レベルの競合解決は、bdr.alter_table_conflict_detection()関数によって管理されます。

例

bdr.alter_table_conflict_detection()の使用方法を説明するために、簡単なテーブルtest_tableを作成し、列レベルの競合解決を有効にする次の例を検討します。

db=# CREATE TABLE my_app.test_table (id SERIAL PRIMARY KEY, val INT);
CREATE TABLE

db=# ALTER TABLE my_app.test_table REPLICA IDENTITY FULL;
ALTER TABLE

db=# SELECT bdr.alter_table_conflict_detection(
db(# 'my_app.test_table'::regclass, 'column_modify_timestamp', 'cts');
 alter_table_conflict_detection
--------------------------------
 t

db=# \d my_app.test_table

ファンクションが新しいcts列を追加することがわかります(ファンクション呼び出しで指定されています)が、各変更の前に新しい列のタイムスタンプを維持する2つのトリガー(BEFORE INSERTとBEFORE UPDATE)も作成しました。

また、言及する価値があるのは、新しい列がNOT NULLをデフォルト値で指定していることです。つまり、ALTER TABLE ... ADD COLUMNはテーブルの書き換えを実行しません。

  • 注*:bdr.column_timestampsデータタイプの列の使用はお勧めしません さまざまな悪影響を与える可能性があるため、他の目的のために (テーブルを列レベルの競合解決に切り替えます。これにより、 トリガーなどがなければ正しく動作しません。)。

列レベルの競合解決を使用したテーブルの一覧表示

列レベルの競合解決が有効になっているテーブルは、bdr.column_timestamp型の列の存在を検出する次のクエリーでリストできます。

SELECT nc.nspname, c.relname
FROM pg_attribute a
JOIN (pg_class c JOIN pg_namespace nc ON c.relnamespace = nc.oid)
   * ON a.attrelid = c.oid
JOIN (pg_type t  JOIN pg_namespace nt ON t.typnamespace = nt.oid)
   * ON a.atttypid = t.oid
WHERE NOT pg_is_other_temp_schema(nc.oid)
   * AND nt.nspname = 'bdr'
   * AND t.typname = 'column_timestamps'
   * AND NOT a.attisdropped
   * AND c.relkind IN ('r', 'v', 'f', 'p');

bdr.column_timestamps_create

このファンクションは、列レベルの競合解決を作成します。これはwithincolumn_timestamp_enableと呼ばれます。

あらすじ

bdr.column_timestamps_create(p_source cstring, p_timestamp timestampstz)

パラメーター

  • p_source-2つのオプションは ‘current’または’ コミット’です。

  • p_timestamp-タイムスタンプは選択したソースに依存します: ’ コミット’の場合、 次にTIMESTAMP_SOURCE_COMMIT; ’current’の場合、TIMESTAMP_SOURCE_CURRENT。

DDLロック

テーブルの列のタイムスタンプを有効または無効にするとき、コードはDDLロックを使用して、切り替え前から保留中の変更がないことを保証し、両方のタプルのタイムスタンプまたは両方のタイムスタンプとの競合のみを保証します。そうしないと、コードが予期せずローカルタプルにタイムスタンプを、リモートタプルにNULLを検出する可能性があります。また、すべてのノードで同じ方法(列レベルまたは行レベル)で変更が解決されるようにします。

現在とコミットのタイムスタンプ

重要な質問は、変更された列に割り当てるタイムスタンプです。

デフォルトでは、変更された列に割り当てられたタイムスタンプは、clock_timestampから取得されたかのようにcurrenttimestampです。これはシンプルで、多くの場合、完全に正しいです(たとえば、競合する行が重複しない列のサブセットを変更する場合)。

ただし、予期しないさまざまな影響がある場合があります。

  • ステートメントの実行中にタイムスタンプが変更されるため、UPDATE マルチプルの行に影響し、それぞれがわずかに異なるタイムスタンプを取得します。 これは、同時変更の影響がさまざまな「混在」になる可能性があることを意味します 方法(変更がどのように正確に実行されたかによって異なります ノードインターリーブ)。

  • タイムスタンプはコミットタイムスタンプとは無関係であり、それを使用して 競合の解決は、結果がコミットオーダーと等しくないことを意味します。 これは、おそらくシリアライザブルできないことを意味します。

注:将来、ステートメントおよびトランザクションのタイムスタンプを追加する可能性があります。これにより、同時ステートメントまたはトランザクションの混合効果に関する問題に対処できます。それでも、これらのオプションはいずれも、コミットオーダーと同等の結果を生成することはできません。

実際のコミットタイムスタンプを使用することもできますが、この機能は現在実験的と見なされています。コミットタイムスタンプを使用するには、列レベルの競合解決を有効にするときに最後のパラメータをtrueに設定します。

SELECT bdr.column_timestamps_enable('test_table'::regclass, 'cts', true);

これは、bdr.column_timestamps_disableを使用して無効にすることもできます。

現在、コミットタイムスタンプには、「制限」セクションで説明されているいくつかの制限があります。

列のタイムスタンプの検査

変更された列のタイムスタンプを保存する列は、トリガーによって自動的に維持されます。直接変更しないでください。例、特定の競合がどのように解決されたかを調査する際に、現在のタイムスタンプ値を調べると便利です。

この目的には3つの機能があります。

  • bdr.column_timestamps_to_text(bdr.column_timestamps)

    • このファンクションは、タイムスタンプマッピングの人間が読み取れる表現形式を返し、値をtextにキャストするときに使用されます。

db=# select cts::text from test_table;
   *                                                cts
-----------------------------------------------------------------------------------------------------
 {source: current, default: 2018-09-23 19:24:52.118583+02, map: [2 : 2018-09-23 19:25:02.590677+02]}
(1 row)
  • bdr.column_timestamps_to_jsonb(bdr.column_timestamps)

    • このファンクションはタイムスタンプマッピングのJSONB表現形式を有効にし、値をjsonbにキャストするときに使用されます。

db=# select jsonb_pretty(cts::jsonb) from test_table;
   *                  jsonb_pretty
---------------------------------------------------
 {                                                +
   *    "map": {                                     +
   *        "2": "2018-09-23T19:24:52.118583+02:00"  +
   *    },                                           +
   *    "source": "current",                         +
   *    "default": "2018-09-23T19:24:52.118583+02:00"+
 }
(1 row)
  • bdr.column_timestamps_resolve(bdr.column_timestamps, xid)

    • このファンクションは、最新のトランザクション(既にコミットされている場合)によって変更された属性のコミットタイムスタンプでマッピングを更新します。これは、コミットタイムスタンプを使用する場合にのみ重要です。例、この場合、最後のトランザクションは2番目の属性を(attnum = 2で)更新しました:

test=# select cts::jsonb from test_table;
   *                                                                 cts
----------------------------------------------------------------------------------------------------------------------------------------
 {"map": {"2": "2018-09-23T19:29:55.581823+02:00"}, "source": "commit", "default": "2018-09-23T19:29:55.581823+02:00", "modified": [2]}
(1 row)

db=# select bdr.column_timestamps_resolve(cts, xmin)::jsonb from test_table;
   *                                              column_timestamps_resolve
-----------------------------------------------------------------------------------------------------------------------
 {"map": {"2": "2018-09-23T19:29:55.581823+02:00"}, "source": "commit", "default": "2018-09-23T19:29:55.581823+02:00"}
(1 row)

CRDTデータ型を使用した列の競合の処理

デフォルトでは、列レベルの競合解決は、単にタイムスタンプの高い値を選択し、他の値を破棄します。ただし、たとえば、情報を破棄せずに競合する値を「マージ」できるCRDTタイプを使用するなど、さまざまな(より複雑な)方法で競合を調整することができます。

制限

  • UPDATEによって変更された属性は、 トリガーの古い行と新しい行。これは、属性が 値を変更しない場合、変更されたとしても検出されません 明示的に設定されます。例、UPDATE t SET a = aはaをマークしません 任意の行に対して変更されます。同様に、UPDATE t SET a = 1はマークません 1に既に設定されている行に対して変更されたa。

  • INSERTステートメントの場合、新しい行と比較する古い行はありません 1つに、すべての属性を変更することを考慮して割り当てます 新しいタイムスタンプ。これは、含まれていない列にも適用されます INSERTステートメントでデフォルト値を受け取りました。検出できた どの属性にはデフォルト値がありますが、次のことを決定することはできません それは自動的に含まれるか、ユーザによって明示的に指定されました。

    • これは事実上、INSERT-INSERTの競合に対して列レベルの競合解決が機能しないことを意味します(INSERTステートメントが列の異なるサブセットを指定している場合でも、新しい行は古い行よりも新しいタイムスタンプを持つため)。

  • 列を個別に処理することにより、コンストレインに違反しやすくなります すべての変更が同じ場所で発生した場合には不可能な方法で ノード例、次のようなテーブルを考えてみましょう。

CREATE TABLE t (id INT PRIMARY KEY, a INT, b INT, CHECK (a > b));
INSERT INTO t VALUES (1, 1000, 1);

… 1つのノードが以下を行うと仮定します。

UPDATE t SET a = 100;

…別のノードが同時に行う場合:

UPDATE t SET b = 500;
  • これらの更新はそれぞれ、最初の行で実行されると有効になるため、各ノードに渡されます。ただし、他のノードに複製する場合、結果の行はCHECK (A > b)制約に違反し、問題が手動で解決されるまでレプリケーションは停止します。

  • タイムスタンプマッピングを格納する列は自動的に管理されます。しないでください クエリで値を指定またはオーバーライドします。 予測不可能な効果(とにかく可能な場合は値を無視します)。

  • タイムスタンプマッピングはトリガーによってオーダーされますが、 トリガーの実行は重要です。したがって、変更するカスタムトリガーがある場合 タプルおよびpgl_clcd_トリガーの後に実行され、変更された 列は正しく検出されません。

  • 変更/コミットをオーダーに通常のタイムスタンプを使用する場合、可能です 競合する変更のタイムスタンプがまったく同じであること(なぜなら 2つ以上のノードがたまたま同じタイムスタンプを生成しました)。このリスク 発生する可能性があるため、列レベルの競合解決に固有ではありません 通常の行レベルの競合解決でも、ノードIDを このシチュエーションでのタイブレーカー(より高いノードIDが勝つ)。 同じ変更がすべてのノードに適用されます。

  • 異なるノード間にクロックスキューがある可能性があります。それながら

    • やや予期しない動作を引き起こす可能性があります

    • タイムスタンプが反転しているために変更されます)、ノード間のクロックスキューは

    • パラメーターbdr.maximum_clock_skewを使用して管理され、

    • bdr.maximum_clock_skew_action。

SELECT bdr.alter_node_group_config('group', ignore_redundant_updates := false);