これは、問題に関連する私のデータベーススキーマです。
これはトリガーの1つです。
CREATE OR REPLACE FUNCTION "public"."update_balance_bet"()
RETURNS "pg_catalog"."trigger" AS $BODY$
DECLARE
currentBalance INTEGER;
BEGIN
currentBalance = (SELECT balance FROM "public"."Users" WHERE id=NEW."UserId");
UPDATE "public"."Users" SET balance=(NEW.amount+currentBalance);
RETURN NEW;
END
$BODY$
CREATE TRIGGER bet_trigger
AFTER INSERT ON "public"."Bets"
FOR EACH ROW
EXECUTE FUNCTION update_balance_bet();
基本的に私のアプリから、テーブルのベット、ボーナス、引き出し、デポジットにレコードを挿入し、残りの作業はトリガーによって行われます。私の質問は、私が使用したメカニズムがすべての場合に一貫性を保証するのか、それとも生のロックを使用する必要があるのかということです。ベットテーブルへの挿入は、トリガー関数がバランスを更新する1つのトランザクションであると想定しているので、ベットへの挿入で問題が発生した場合、バランスは更新されませんOR何か問題が発生した場合)バランスの更新、新しいレコードはベットテーブルに挿入されませんか?
まず、このUPDATEにはバグがあります。
UPDATE "public"."Users" SET balance=(NEW.amount+currentBalance);
これは、「Users」テーブル全体を更新するためです。これは明らかに必要なものではありません。
WHERE句が追加され、コードが次のようになると仮定します。
DECLARE
currentBalance INTEGER;
BEGIN
currentBalance = (SELECT balance FROM "public"."Users" WHERE id=NEW."UserId");
UPDATE "public"."Users" SET balance=(NEW.amount+currentBalance);
WHERE id=NEW."UserId"
RETURN NEW;
このコードは、デフォルトの分離レベル(読み取りコミット)の下で競合状態の影響を受けます。このトランザクションがT1で、別のトランザクションがT2であるとします。 T2がこのユーザーの残高を更新していて、SELECTとUPDATEの間でコミットした場合、T1のUPDATEによって行が上書きされ、T2の変更が失われ、残高が正しくなくなります。
このロジックをコンカレントセーフな方法で実装するには、SQL標準の推奨事項は、シリアル化可能な分離レベルでトランザクションを使用することです。 PostgreSQLドキュメントの トランザクション分離 を参照してください。または、競合状態を回避するために、分離度の低いレベルで独自のロック戦略を実装することもできますが、それを正しく行うのは困難です。
Q:Betsテーブルへの挿入は、残高を更新するトリガー関数と一緒に1つのトランザクションであると仮定して正しいので、Betsへの挿入で問題が発生した場合、残高は更新されませんORバランスの更新で問題が発生した場合、新しいレコードはベットテーブルに挿入されません
はい、しかしそれはトリガーではなくトランザクションのおかげです。通常、SQLステートメントのリストをトリガーに移動するのではなく、BEGIN...COMMIT
ペアの間に配置することでアトミック性を取得します。トリガーのポイントは、メインロジックではなく、副作用を実装することです。トム・カイトの引用 トリガーのトラブル 優れた投稿:
トリガーは懐疑的な目で見る必要があります。エンティティの整合性を強制するためのトリガーが設定されている場合は、それを非常に疑ってください。マルチユーザーの状態について考えてください。 2人または3人が同じデータを同時に、またはほぼ同時に操作するとどうなるか考えてみてください。
[...]
トリガーは例外であり、ルールではありません。他の方法で何かできない場合にのみ使用してください。並行性の問題、それらの中で非トランザクション操作を実行することに関する問題、および保守の問題を考えると、トリガーは、たとえあったとしても、控えめに使用するものです。