5

既存のレコードを非アクティブ化し、新しいレコードを挿入するSaveApp()メソッドがあります。

void SaveApp(int appID)
{
   begin transaction;
   update;
   insert;
   commit transaction;
}

データベーステーブルSalesAppに、appIDが123のレコードが2つあるとします。

  1. レコード1、appID 123、非アクティブ
  2. レコード2、appID 123、アクティブ

このメソッドSaveApp()を2つのスレッドで同時に呼び出すと、最初のトランザクション(T1と呼びます)は既存の2つのレコードを更新し、2番目のトランザクション( T2と呼びます)は待機します。

T1が終了すると、このテーブルには3つのレコードが作成されます。ただし、どういうわけかT2は新しく挿入されたレコードを認識せず、T2の更新クエリは前の2つのレコードのみを更新し、4番目のレコードを挿入します。

これらの2つのメソッド呼び出しの後、データベースに4つのレコードがあり、3番目と4番目のレコードが両方ともアクティブになっていますが、これは間違っています。

  1. レコード1、appID 123、非アクティブ
  2. レコード2、appID 123、非アクティブ
  3. レコード3、appID 123、アクティブ
  4. レコード4、appID 123、アクティブ

この問題を解決できる解決策を知っていますか?動作しないシリアル化可能な分離レベルを使用してみました。

ありがとう!

4

8 に答える 8

6

AppId ごとに 1 つの行を保持する別のテーブルがあり、一意または主キーの制約によって適用されますか? その場合は、親テーブルで使用select for updateして、AppId ごとにアクセスをシリアル化します。

テーブルを作成します。

session_1> create table parent (AppId number primary key);

Table created.

session_1> create table child (AppId number not null references Parent(AppId)
  2      , status varchar2(1) not null check (status in ('A', 'I'))
  3      , InsertedAt date not null)
  4  /

Table created.

開始値を挿入:

session_1> insert into Parent values (123);

1 row created.

session_1> insert into child values (123, 'I', sysdate);

1 row created.

session_1> insert into child values (123, 'A', sysdate);

1 row created.

session_1> commit;

Commit complete.

最初のトランザクションを開始します。

session_1> select AppId from Parent where AppId = 123 for update;

     APPID
----------
       123

session_1> update Child set Status = 'I' where AppId = 123 and Status = 'A';

1 row updated.

session_1> insert into child values (123, 'A', sysdate);

1 row created.

コミットする前に、2 番目のセッションで、最初の行のみが表示されていることを確認します。

session_2> select * from Child;

     APPID S INSERTEDAT
---------- - -------------------
       123 I 2010-08-16 18:07:17
       123 A 2010-08-16 18:07:23

2 番目のトランザクションを開始します。

session_2> select AppId from Parent where AppId = 123 for update;

セッション 2 は現在ブロックされており、セッション 1 を待機しています。セッション 1 をコミットすると、セッションのブロックが解除されます

session_1> commit;

Commit complete.

セッション 2 では、次のことがわかります。

     APPID
----------
       123

2 番目のトランザクションを完了します。

session_2> update Child set Status = 'I' where AppId = 123 and Status = 'A';

1 row updated.

session_2> insert into child values (123, 'A', sysdate);

1 row created.

session_2> commit;

Commit complete.

session_2> select * from Child;

     APPID S INSERTEDAT
---------- - -------------------
       123 I 2010-08-16 18:07:17
       123 I 2010-08-16 18:07:23
       123 I 2010-08-16 18:08:08
       123 A 2010-08-16 18:13:51

EDIT Technique は、Expert Oracle Database Architectureの第 2 版、Thomas Kyte の 23 ~ 24 ページから引用されています。http://www.amazon.com/Expert-Oracle-Database-Architecture-Programming/dp/1430229462/ref=sr_1_2?ie=UTF8&s=books&qid=1282061675&sr=8-2

編集 2また、AppId がアクティブなレコードを 1 つだけ持つことができるという規則を強制する制約について、この質問に対する Patrick Merchand の回答を実装することをお勧めします。したがって、最終的な解決策には2つの部分があります。これは、必要なものを取得する方法で更新を行う方法に関する回答と、データの整合性を保護するための要件にテーブルが準拠していることを確認するパトリックの回答です。

于 2010-08-16T22:15:44.643 に答える
4

特定のIDのデータベースに複数の「アクティブ」レコードを含めることができないようにする場合は、次のようにします(クレジットはここにあります): http://asktom.oracle.com/pls/apex/f? p = 100:11:0 :::: P11_QUESTION_ID:1249800833250

Oracleが完全にNULLのインデックスエントリを格納しないという事実を利用して、特定のIDが複数の「アクティブな」レコードを持つことができないことを保証します。

drop table test
/

create table test (a number(10), b varchar2(10))
/

CREATE UNIQUE INDEX unq ON test (CASE WHEN b = 'INACTIVE' then NULL ELSE a END)
/

これらのインサートは正常に機能します。

insert into test (a, b) values(1, 'INACTIVE');
insert into test (a, b) values(1, 'INACTIVE');
insert into test (a, b) values(1, 'INACTIVE');
insert into test (a, b) values(1, 'ACTIVE');
insert into test (a, b) values(2, 'INACTIVE');
insert into test (a, b) values(2, 'INACTIVE');
insert into test (a, b) values(2, 'INACTIVE');
insert into test (a, b) values(2, 'ACTIVE');

これらの挿入は失敗します:

insert into test values(1, 'ACTIVE');

ORA-00001:一意の制約(SAMPLE.UNQ)に違反しました

insert into test values(2, 'ACTIVE');

ORA-00001:一意の制約(SAMPLE.UNQ)に違反しました

于 2010-08-17T00:07:09.327 に答える
1

昨日、説明した問題を再現するテスト ケースを作成しました。今日、テストケースに欠陥があることがわかりました。問題が理解できなかったので、昨日の回答は間違っていると思います。

次の 2 つの問題が考えられます。

  1. とのcommit間にハプニングがあります。updateinsert

  2. これは、新しい のみの問題です AppId

テストケース:

テスト テーブルを作成し、2 つの行を挿入します。

session 1 > create table test (TestId number primary key
  2             , AppId number not null
  3             , Status varchar2(8) not null 
  4                 check (Status in ('inactive', 'active'))
  5  );

Table created.

session 1 > insert into test values (1, 123, 'inactive');

1 row created.

session 1 > insert into test values (2, 123, 'active');

1 row created.

session 1 > commit;

Commit complete.

最初のトランザクションを開始します:

session 1 > update test set status = 'inactive'
  2         where AppId = 123 and status = 'active';

1 row updated.

session 1 > insert into test values (3, 123, 'active');

1 row created.

2 番目のトランザクションを開始します。

session 2 > update test set status = 'inactive'
  2         where AppId = 123 and status = 'active';

セッション 2 はブロックされ、行 2 の行ロックの取得を待機しています。セッション 2 は、セッション 1 のトランザクションがコミットまたはロールバックされるまで続行できません。セッション 1 をコミットします。

session 1 > commit;

Commit complete.

セッション 2 のブロックが解除され、次のように表示されます。

1 row updated.

セッション 2 のブロックが解除されると、更新ステートメントが再開され、セッション 1 の変更が確認され、行3が更新されました。

session 2 > select * from test;

    TESTID      APPID STATUS
---------- ---------- --------
         1        123 inactive
         2        123 inactive
         3        123 inactive

セッション 2 でトランザクションを完了します。

session 2 > insert into test values (4, 123, 'active');

1 row created.

session 2 > commit;

Commit complete.

結果を確認します (セッション 1 を使用):

セッション 1 > テストから * を選択します。

    TESTID      APPID STATUS
---------- ---------- --------
         1        123 inactive
         2        123 inactive
         3        123 inactive
         4        123 active

update2 つの が互いにブロックしないようにする唯一の方法は、一方と他方の間でコミットまたはロールバックを行うことです。使用しているソフトウェア スタックのどこかに暗黙のコミットが隠されている可能性があります。私は .NET について十分に理解していないため、.NET を追跡することについてアドバイスすることはできません。

ただし、AppId がテーブルにとって新しい場合、同じ問題が発生します。456 の新しい AppId を使用してテストします。

session 1 > update test set status = 'inactive'
  2         where AppId = 456 and status = 'active';

0 rows updated.

行が書き込まれないため、ロックは取得されません。

session 1 > insert into test values (5, 456, 'active');

1 row created.

同じ新しい AppId の 2 番目のトランザクションを開始します。

session 2 > update test set status = 'inactive'
  2          where AppId = 456 and status = 'active';

0 rows updated.

セッション 2 は行 5 を認識しないため、行 5 に対するロックを取得しようとはしません。セッション 2 を続行します。

session 2 > insert into test values (6, 456, 'active');

1 row created.

session 2 > commit;

Commit complete.

セッション 1 をコミットして結果を表示します。

session 1 > commit;

Commit complete.

session 1 > select * from test;

    TESTID      APPID STATUS
---------- ---------- --------
         1        123 inactive
         2        123 inactive
         3        123 inactive
         4        123 active
         5        456 active
         6        456 active

6 rows selected.

修正するには、Patrick Marchand ( Oracle トランザクション分離)の関数ベースのインデックスを使用します。

session 1 > delete from test where AppId = 456;

2 rows deleted.

session 1 > create unique index test_u
  2         on test (case when status = 'active' then AppId else null end);

Index created.

新しい AppId の最初のトランザクションを開始します。

session 1 > update test set status = 'inactive'
  2         where AppId = 789 and status = 'active';

0 rows updated.

session 1 > insert into test values (7, 789, 'active');

1 row created.

ここでも、セッション 1 は更新でロックを取得しません。行 7 に書き込みロックがあります。2 番目のトランザクションを開始します。

session 2 > update test set status = 'inactive'
  2         where AppId = 789 and status = 'active';

0 rows updated.

session 2 > insert into test values (8, 789, 'active');

ここでも、セッション 2 は行 7 を認識しないため、行 7 をロックしようとはしません。しかし、挿入は関数ベースのインデックスの同じスロットに書き込もうとしており、セッション 1 によって保持されている書き込みロックのブロックです。セッション 2 はセッション 1 がcommitorになるのを待ちますrollback:

session 1 > commit;

Commit complete.

セッション 2 は次のとおりです。

insert into test values (8, 789, 'active')
*
ERROR at line 1:
ORA-00001: unique constraint (SCOTT.TEST_U) violated

その時点で、クライアントはトランザクション全体を再試行できます。(updateと の両方insert。)

于 2010-08-17T20:05:28.943 に答える
0

@Alexは正しいです。これは、実際にはOracleの問題ではなく、アプリケーションの問題です。

おそらく、このようなものがあなたのために働くかもしれません:

Oracleトランザクションをストアドプロシージャに入れ、次のように実行します。

BEGIN
  LOOP
    BEGIN
      SELECT * 
        FROM SaleApp
       WHERE appID = 123
         AND status = 'ACTIVE'
         FOR UPDATE NOWAIT;
      EXIT;
    EXCEPTION
      WHEN OTHERS THEN
        IF SQLCODE = -54 THEN
          NULL;
        ELSE
          RAISE error
        END IF;
    END IF;
  END LOOP;
  UPDATE ....
  INSERT ....
  COMMIT;
END;

ここでの考え方は、現在アクティブなレコードを取得してロックする最初のトランザクションが完了するということです。そのレコードをロックしようとする他のトランザクションは、SELECT FOR UPDATE NOWAITで失敗し、成功するまでループします。

通常のトランザクションの実行にかかる時間によっては、選択を再試行する前に、例外ハンドラー内でスリープすることをお勧めします。

于 2010-08-16T21:31:31.217 に答える
0

「3 番目と 4 番目の両方がアクティブですが、これは間違っています。」

単純な一意のインデックスは、データベース レベルでそれを防ぐことができます。

create table rec (id number primary key, app_id number, status varchar2(1));
create unique index rec_uk_ix on rec (app_id, case when status = 'N' then id end);
insert into rec values (1,123,'N');
insert into rec values (2,123,'N');
insert into rec values (3,123,'N');
insert into rec values (4,123,'Y');
insert into rec values (5,123,'Y');

一意のインデックスにより、ステータスが「N」以外のアプリのレコードが 1 つだけ存在することが保証されます。

明らかに、アプリケーションはエラーをキャッチし、それをどう処理するかを知る必要があります (再試行するか、データが変更されたことをユーザーに通知します)。

于 2010-08-16T23:02:58.690 に答える
0

実際には Oracle の問題ではないようです。アプリケーションの同時実行の問題です。これが何語なのかわからない; それがJavaの場合はsynchronise、メソッドだけでできますか?

于 2010-08-16T21:16:18.923 に答える
0

完全にはわかりませんが、両方のトランザクションを SERIALIZABLE に設定すると、2 番目のトランザクションでエラーが発生し、何かが間違っていることがわかると思います。

于 2010-08-16T21:43:48.133 に答える
0

更新をキュー (おそらく AQ) にプッシュして、順次実行できるようにすることはできますか?

別のオプションは、問題のレコードをロックすることです (SELECT FOR UPDATE NOWAIT または SELECT FOR UPDATE WAIT)。

于 2010-08-16T21:14:55.680 に答える