Column-level conflict detection¶
デフォルトでは、競合は行レベルで解決されます。つまり、2つのノードからの変更が競合する場合、ローカルまたはリモートのタプルを選択し、もう一方を破棄します。たとえば、2つの競合する変更のコミットタイムスタンプを比較し、新しい方を保持する場合があります。これにより、すべてのノードが同じ結果に収束し、クラスター全体でコミットオーダーのようなセマンティクスが確立されます。
ただし、行レベルではなく列レベルで競合を解決する方が適切な場合があります。
2つの整数列aとb、および単一の行(1,1)
を持つテーブルtがある簡単な例を考えます。
1つのノードで実行すると仮定します。
UPDATE t SET a = 100
別のノードで同時に(先行のUPDATE を受け取る前に)実行します。
UPDATE t SET b = 100
これにより、 UPDATE-UPDATE の競合が発生します。 update_if_newer
競合解決では、コミットのタイムスタンプを比較し、新しい行バージョンを保持します。
2番目のノードが最後にコミットされたと仮定すると、最終的に(1,100)
になり、列aへの変更を効果的に破棄します。
多くのユースケースでは、これは望ましい動作ですが、問題になる場合があります。たとえば、アプリケーションの各部分が異なるノードに接続され、共有テーブル内の列の専用サブセットを更新するマルチノードクラスターを考えます。その場合、さまざまなコンポーネントがお互いのつま先を踏んで、変更を上書きする可能性があります。
このようなユースケースでは、特定のテーブルの競合を列レベルで解決する方が適切な場合があります。これを行うために、
BDRは各列の最終変更のタイムスタンプを個別に追跡し、それを使用して最新の値(基本的にupdate_if_newer
)を選択します。
前の例に適用すると、どちらのノードもそのような行を表示していないにもかかわらず、両方のノードで(100,100)
になります。
列レベルの競合解決を検討するときは、テーブルを垂直にパーティション分割されたものとして表示すると、各更新が1つのスライスのデータにのみ影響する場合があります。このアプローチにより、列の異なるサブセットに対する変更間の競合が排除されます。実際、垂直方向のパーティション化は、列レベルの競合解決の実用的な代替手段になることさえあります。
列レベルの競合解決では、テーブルにREPLICA IDENTITY FULL
が必要です。 bdr.alter_table_conflict_detection
関数はそれをチェックし、それ以外の場合はエラーで失敗します。
列レベルの競合解決の有効化と無効化¶
列レベルの競合解決は、 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¶
このファンクションは、列レベルの競合解消を作成します。
column_timestamp_enable 内で呼び出されます。
概要¶
bdr.column_timestamps_create(p_source cstring, p_timestamp timestampstz)
パラメーター¶
p_source— 2つのオプションは、currentまたはcommitです。p_timestamp—タイムスタンプは、選択したソースによって異なります。commitの場合、TIMESTAMP_SOURCE_COMMIT。currentの場合、TIMESTAMP_SOURCE_CURRENT。
DDLロック¶
テーブルの列のタイムスタンプを有効または無効にする場合、コードはDDLロックを使用して、切り替え前からの保留中の変更がないことを確認します。このアプローチにより、両方のタプルまたはどちらでもタイムスタンプの競合のみが表示されます。そうしないと、コードはローカルのタプルにタイムスタンプを、リモートのタプルに NULL を予期せず表示する場合があります。また、変更がすべてのノードで同じ方法(列レベルまたは行レベル)で解決されるようにします。
現在とコミットのタイムスタンプ¶
重要な決定は、変更された列に割り当てるタイムスタンプです。
デフォルトでは、変更された列に割り当てられるタイムスタンプは、あたかも
clock_timestamp
から取得されたかのように、現在のタイムスタンプです。これは簡単で、多くの場合、完全に正しいです(たとえば、競合する行が列の重複しないサブセットを変更する場合)。
ただし、さまざまな予期しない効果があります。
タイムスタンプはステートメントの実行中に変更されるため、
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ステートメントの場合、新しい行と比較する古い行がないため、すべての属性が変更されると見なし、新しいタイムスタンプを割り当てます。これは、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);