Почему строка видна нескольким сеансам, если выбрано ЗАПУСК ОБНОВЛЕНИЯ ЗАБЛОКИРОВАН? - PullRequest
0 голосов
/ 26 февраля 2019

Предположим, есть две таблицы TST_SAMPLE (10000 rows) и TST_SAMPLE_STATUS (empty).

. Я хочу перебрать каждую запись в TST_SAMPLE и добавить ровно одну запись к TST_SAMPLE_STATUS соответственно.

В одном потоке это было бы просто так:

begin
  for r in (select * from TST_SAMPLE)
  loop 

    insert into TST_SAMPLE_STATUS(rec_id, rec_status)
    values (r.rec_id, 'TOUCHED');

  end loop;

  commit;
end;
/

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

Пожалуйста, см. Подробности ниже.

create table TST_SAMPLE(
  rec_id       number(10) primary key 
);

create table TST_SAMPLE_STATUS(
  rec_id       number(10),
  rec_status   varchar2(10),
  session_id   varchar2(100)
);


begin
  insert into TST_SAMPLE(rec_id)
  select LEVEL from dual connect by LEVEL <= 10000;

  commit;
end;
/


CREATE OR REPLACE PROCEDURE tst_touch_recs(pi_limit int) is
  v_last_iter_count int;
begin

   loop

     v_last_iter_count := 0;

     --------------------------
     for r in (select *
                 from TST_SAMPLE A
                where rownum < pi_limit
                  and NOT EXISTS (select null
                                    from TST_SAMPLE_STATUS B
                                   where B.rec_id = A.rec_id)
                  FOR UPDATE SKIP LOCKED)
     loop

        insert into TST_SAMPLE_STATUS(rec_id, rec_status, session_id)
        values (r.rec_id, 'TOUCHED', SYS_CONTEXT('USERENV', 'SID'));

        v_last_iter_count := v_last_iter_count + 1;
     end loop;

     commit;
     --------------------------

     exit when v_last_iter_count = 0;

   end loop;
end;
/

В FOR-LOOP я пытаюсьперебирать строки, которые: - не имеют статуса (предложение NOT EXISTS) - в настоящее время не заблокированы в другом потоке (FOR UPDATE SKIP LOCKED)

Нет точного количества строк в итерации,Здесь pi_limit - это максимальный размер одной партии.Нужно только обработать каждую строку TST_SAMPLE ровно за один сеанс.

Итак, давайте запустим эту процедуру в 3 потока.

declare
 v_job_id number;
begin

  dbms_job.submit(v_job_id, 'begin tst_touch_recs(100); end;', sysdate);
  dbms_job.submit(v_job_id, 'begin tst_touch_recs(100); end;', sysdate);
  dbms_job.submit(v_job_id, 'begin tst_touch_recs(100); end;', sysdate);

  commit;
end;

Неожиданно мы видим, чтонекоторые строки были обработаны за несколько сеансов

select count(unique rec_id) AS unique_count,
       count(rec_id)        AS total_count
  from TST_SAMPLE_STATUS;


| unique_count | total_count |
------------------------------
|        10000 |       17397 |
------------------------------


-- run to see duplicates
select * 
  from TST_SAMPLE_STATUS 
 where REC_ID in (
                    select REC_ID 
                      from TST_SAMPLE_STATUS
                     group by REC_ID
                    having count(*) > 1
                 )
 order by REC_ID;

Пожалуйста, помогите распознать ошибки в реализации процедуры tst_touch_recs.

1 Ответ

0 голосов
/ 26 февраля 2019

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

Запустите следующий код в двух сеансах, начиная второй через несколько секунд после первого:

declare
  cursor c is 
    select a.*
     from TST_SAMPLE A
    where rownum < 10
      and NOT EXISTS (select null
                        from TST_SAMPLE_STATUS B
                       where B.rec_id = A.rec_id)
      FOR UPDATE SKIP LOCKED;

  type rec is table of c%rowtype index by pls_integer;
  rws rec;
begin
  open c; -- data are read consistent to this time

  dbms_lock.sleep ( 10 );

  fetch c 
  bulk  collect 
  into  rws;

  for i in 1 .. rws.count loop
    dbms_output.put_line ( rws(i).rec_id );
  end loop;

  commit;

end;
/

Вы должны увидеть, что оба сеанса отображают одинаковые строки.

Почему?

Поскольку база данных Oracle имеет согласованность на уровне операторов, при открытии курсора набор результатов для обоих будет заморожен.

Но когда у вас заблокирован SKIP, блокировка FOR UPDATE включается только , когда вы выбираете строки .

Итак, сессия 1 начинается и находит первые 9 строк, не входящих в TST_SAMPLE_STATUS.Затем он ждет 10 секунд.

Если вы начнете сеанс 2 в течение этих 10 секунд, курсор будет искать те же девять строк .

В этот момент никакие строки не заблокированы.

Теперь вот где это становится интересным.

Сон в первом сеансе закончится.Затем он извлекает строки, блокируя их и пропуская все, что уже заблокировано.

Очень скоро после этого он совершит. Снятие блокировки .

Через несколько секунд сеанс 2 начинает читать эти строки.На данный момент строки не заблокированы !

Так что пропускать нечего.

То, как именно вы решите это, зависит от того, что вы пытаетесь сделать.

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

set transaction isolation level serializable;

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

Но вам нужно поймать ORA-08177: can't serialize access for this transaction ошибок в вашем внешнем цикле.Или любой процесс, который перечитывает те же строки, в этот момент выпадет.

Или, как предложили комментаторы, использовали Advanced Queuing.

...