Изоляция транзакций Oracle - PullRequest
       62

Изоляция транзакций Oracle

5 голосов
/ 17 августа 2010

У меня есть метод SaveApp (), который деактивирует существующие записи и вставляет новую.

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

Допустим, в таблице базы данных SalesApp у меня есть 2 записи с appID, равным 123;

  1. запись 1, appID 123, неактивный
  2. запись 2, appID 123, активный

Если я вызываю этот метод SaveApp() в двух потоках одновременнопервая транзакция (назовем ее T1 ) обновит существующие две записи, в то время как вторая транзакция (назовем ее T2 ) ожидает.

после завершения T1 в этой таблице будет три записи.однако каким-то образом T2 не знает о вновь вставленной записи, запрос на обновление в T2 обновляет только две предыдущие записи и вставляет четвертую.

послеэти два вызова метода, в базе данных, теперь у нас будет 4 записи, 3-я и 4-я активны, что неверно.

  1. запись 1, appID 123, неактивная
  2. запись2, appID 123, неактивный
  3. запись 3, appID 123, активный
  4. запись 4, appID 123, активный

Знаете ли вы, что какое-либо решение может решить эту проблему?Я пробовал использовать сериализуемый уровень изоляции, который не работает.

Спасибо!

Ответы [ 8 ]

6 голосов
/ 17 августа 2010

Есть ли у вас другая таблица, которая содержит одну строку для каждого AppId, примененную через ограничение уникального или первичного ключа? Если это так, используйте select for update в родительской таблице для сериализации доступа по идентификатору приложения.

Создание таблиц:

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.

Перед фиксацией во втором сеансе убедитесь, что мы видим только первые строки:

session_2> select * from Child;

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

Запустить вторую транзакцию:

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

Сессия 2 теперь заблокирована, ожидание на сессии 1. И не будет продолжаться. Фиксация сеанса 1 разблокирует сеанс

session_1> commit;

Commit complete.

Сессия 2 теперь мы видим:

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

Завершите вторую транзакцию:

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

РЕДАКТИРОВАТЬ Техника взята из Эксперт Oracle Database Architecture Второе издание Томаса Кайта, страницы 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

EDIT 2 Я бы также рекомендовал реализовать ответ Патрика Мерчанда на этот вопрос для ограничения, которое применяет правило, согласно которому в AppId может быть только одна активная запись. Таким образом, окончательное решение будет состоять из двух частей: это ответ о том, как делать обновления таким образом, чтобы получить то, что вы хотите, и Патрик, чтобы убедиться, что таблица соответствует требованиям для защиты целостности данных.

4 голосов
/ 17 августа 2010

Если вы хотите убедиться, что никогда не можете иметь более одной «активной» записи в БД для данного идентификатора, вот круто (кредит здесь): http://asktom.oracle.com/pls/apex/f?p=100:11:0::::P11_QUESTION_ID:1249800833250

Он использует тот факт, что Oracle не хранит полностью записи индекса NULL и гарантирует, что конкретный идентификатор не может иметь более одной "активной" записи:

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) нарушено

1 голос
/ 18 августа 2010

Вчера я создал контрольный пример для воспроизведения описанной проблемы.Сегодня я обнаружил, что контрольный пример был ошибочным.Я не понял проблему, поэтому я считаю, что ответ, который я дал вчера, неверен.

Возможны две проблемы:

  1. Существует commit происходит между update и insert.

  2. Это только проблема для новых AppId с.

Контрольный пример:

Создайте тестовую таблицу и вставьте две строки:

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.

Начните вторую транзакцию:

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

Единственный способ для двух update не блокировать друг друга - это сделать коммит или откат между одним и другим.Может быть неявная фиксация, скрытая где-то в программном стеке, который вы используете.Я не знаю достаточно о .NET, чтобы посоветовать отследить это.

Однако та же проблема произойдет, если AppId является новым для таблицы.Протестируйте с использованием нового AppId 456:

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:

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

0 rows updated.

Сессия 2 не видит строку 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.

Для исправления используйте индекс на основе функций от Патрика Маршана ( Изоляция транзакции 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 установлена ​​блокировка записи. Запустите вторую транзакцию:

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, поэтому он не пытается ее заблокировать. НО вставка пытается выполнить запись в тот же слот по индексу, основанному на функции, и блокирует блокировку записи, удерживаемую сеансом 1. Теперь сеанс 2 будет ожидать сеанса 1 до commit или 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.)

0 голосов
/ 17 августа 2010

"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'.

Очевидно, что приложение должно отловить ошибку и знать, что с ней делать (повторите попытку или сообщите пользователю, что данные изменились).

0 голосов
/ 17 августа 2010

Я не совсем уверен, но я думаю, что если вы установите обе транзакции в SERIALIZABLE, вы получите ошибку во второй, чтобы вы знали, что что-то не так.

0 голосов
/ 17 августа 2010

@ Алекс прав, это не проблема 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 и будут циклически повторяться до тех пор, пока не будут выполнены успешно.

В зависимости от того, сколько времени потребуется для выполнения типичной транзакции, вы можете поспать в обработчике исключений перед повторной попыткой выбора.

0 голосов
/ 17 августа 2010

Похоже, это не проблема Oracle, это проблема параллелизма в вашем приложении.Не уверен, что это за язык;если это Java, можете ли вы просто synchronise метод?

0 голосов
/ 17 августа 2010

Можете ли вы поместить обновления в очередь (возможно, AQ), чтобы они выполнялись последовательно?

Другой вариант может заключаться в блокировке рассматриваемых записей (SELECT FOR UPDATE NOWAIT или SELECT FOR UPDATE WAIT)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...