Узгодженість даних при зміні

Цей стаття присвячена проблемі узгодженості даних при змінах в СУБД Oracle. За мотивами цікавого обговорення на сайті Тома Кайта.

Я завжди виходив з припущення, що оператор типу update t set … where … обробляється наступним чином:


  1. Береться узгоджений моментальний знімок таблиці на момент початку виконання оператора
  2. Для кожного рядка цього моментального знімку:
    почекати зняття блокування, якщо рядок заблокована інший транзакцією,
    а потім змінити рядок, якщо вона задовольняє конструкції where.

Іншими словами, будь-які зміни, зроблені транзакціями, що почалися і зафіксованими після початку виконання оператора (визначається за значенням SCN), Невидимі для оператора, оскільки вони не входять до "узгоджений моментальний знімок".


Наступний експеримент (побудований на основі реальної ситуації) змусив мене задуматися про це ще раз.


Ми будемо змінювати велику таблицю за допомогою оператора update (В транзакції, позначеної LONG), Але ми заблокуємо її по ходу роботи – вона буде чекати, поки транзакція BLOCKER не зніме блокування. Поки транзакція LONG чекає, ми змінимо першу і останню рядок з допомогою іншої транзакції, FIRSTLAST, Зафіксуємо її, а потім відкотився транзакцію BLOCKER, Щоб LONG могла продовжити роботу.


Потім ми повторимо ті ж дії, але зафіксуємо транзакцію BLOCKER, Замість того, щоб відкотити її.

 create table t (id int, y int, filler char (10) default "") initrans 2 nologging
storage (initial 1M next 1m pctincrease 0 maxextents unlimited);
insert /*+ APPEND */ into t select rownum, 0, ” ”
from all_objects, all_objects, all_objects where rownum <= 1000000;
commit;

BLOCKER> update t set y = -1 where id = 500000;

LONG> update t set y = rownum where y = -1 or id = 500000; – вона блокується на рядку 500000

FIRSTLAST> update t set y = -1 where id in (1,1000000);
FIRSTLAST> commit;

BLOCKER> rollback;

LONG> select * from t where y <> 0;

ID Y FILLER
———- ———- ———-
1 -1
500000 1
1000000 -1


Цього я і очікував; оскільки транзакція FIRSTLAST була розпочата і зафіксована після транзакції LONG, Її зміни не входять в "моментальний знімок конструкції where" транзакції LONG, І тому перша і остання рядки не були змінені транзакцією LONG [Тобто LONG бачить старе значення (0) Стовпця y першій та останній рядки, хоча нове значення (-1) І задовольняє умові у конструкції where транзакції LONG]. Поки все зрозуміло.


Тепер ми повторимо той же експеримент (повернувши перш значення 0 у стовпці y у всіх рядках таблиці), але зафіксуємо транзакцію BLOCKER замість відкату:

update t set y = 0 where y <> 0;
commit;

BLOCKER> update t set y = -1 where id = 500000;

LONG> update t set y = rownum where y = -1 or id = 500000; – вона блокується на рядку 500000

FIRSTLAST> update t set y = -1 where id in (1,1000000);
FIRSTLAST> commit;

BLOCKER> commit; – Це єдина відмінність

LONG> select * from t where y <> 0;

ID Y FILLER
———- ———- ———-
1 1
500000 2
1000000 3


Тепер, хоча транзакція FIRSTLAST розпочато і зафіксована після початку LONG, Так що її зміни не входять в "моментальний знімок конструкції where" транзакції LONG, LONG, Фактично, побачила зміни, виконані FIRSTLAST (А не тільки зміни, виконані транзакцією BLOCKER, Що природно).


Цей експеримент дає однакові результати у версіях 8.1.7 та 9.0.1.


Якщо оператор update транзакції LONG відкотити і виконати заново після визначення того, що транзакція BLOCKER змінила рядок, ми отримаємо такий самий результат.


Перше питання. Чи виконує Oracle оператор заново насправді? Точніше, чи можу я розробляти ПЗ з урахуванням перезапуску оператора як способу забезпечення узгодженості при одночасних змінах, навіть якщо на самому справі сервер Oracle робить щось, аналогічне за результатами такого перезапуску?


Друге питання. У чому причина такої поведінки? Інтуїція мені підказує, що це забезпечує облік усіх змін транзакції BLOCKER або жодного, тобто забезпечує неподільність (atomicity) транзакції BLOCKER… але я б хотів отримати підтвердження або спростування цієї здогадки.


[Примітка: Транзакція LONG присвоює стовпцю y значення rownum, І якщо згадати, що значення rownum присвоюється рядкам у міру їх перегляду оператором, здається, що транзакція LONG змінила рядка, починаючи з id = 1, потім – 500000, потім – 1000000, навіть якщо вона була заблокована на id = 500000, і вже зверталася до id = 1 і відкинула цей рядок (як випливає з мого припущення про поведінку сервера Oracle в даному випадку, – я знаю, що в загальному випадку порядок перегляду не можна ні передбачити, ні відтворити). Просто пожива для роздумів – з цього нічого не треба.]


Представлений вище експеримент побудований на основі реальної виробничої ситуації, оператор update якої був схожий на використаний у транзакції LONG, І застосовувався до великої таблиці, в якій одночасно виконувалися (безліч) змін, аналогічних виконуваним у транзакціях BLOCKER і FIRSTLAST. Я можу легко уявити й інші ситуації, коли треба виконати пакетне оновлення великої таблиці, яка приймає транзакції, з великою кількістю змін (або вставок і вилучень) в секунду, причому, цілодобових. Наприклад, "підвищити зарплату приблизно опівночі всім співробітникам, які …". "Приблизно" означає "якомога ближче до півночі", – змінити всю таблицю в 00:05 можна, але аж ніяк не о 13:00 …


Якщо припустити, що відповідь на перше питання – "так", я не можу просто запустити пакетне оновлення близько получночі, оскільки набір рядків для зміни не визначається однозначно відразу, а уточнюється при кожному змінити рядок будь-якої наступної транзакцією.


Якщо розглянути крайній випадок, може знадобитися перезапускати оператор нескінченно … і ніякого узгодженого зміни по стану "близько півночі" вже ніяк не вийде.


Ймовірно, краще всього це робити приблизно так (реальна реалізація може виявитися більш складною, використовувати тимчасові таблиці і т.п.):

 create table snap (row_id rowid) as select rowid row_id from t where …

А потім update t на базі snap, Яка містить набір рядків, які необхідно змінити.


Третє питання. Може, я даремно переживаю або просто помиляюся, розглядаючи такий спосіб зміни?


Відповідь Тома Кайта


Я навіть боюся публікувати цю відповідь, оскільки підозрюю, що він викличе шквал коментарів.


Результуюче безліч погоджено – але воно може виявитися узгодженим і на момент перезапуску. Це можна продемонструвати на куди більш простому прикладі – я використовую всього 5 рядків спеціальним чином, аналогічно ваш приклад.


Ми використовуємо рядковий тригер і автономну транзакцію, щоб побачити, що відбувається за кадром. Ось як будується приклад:

ops$tkyte@ORA920> create sequence s;

Sequence created.

ops$tkyte@ORA920> create table msg1
2 ( seq int primary key, sid int,
3 old_id int, old_y int,
4 new_id int, new_y int );

Table created.

ops$tkyte@ORA920> create table msg2
2 ( seq int primary key, sid int,
3 old_id int, old_y int,
4 new_id int, new_y int );

Table created.

ops$tkyte@ORA920> create table t
2 as
3 select rownum id, 0 y
4 from all_users
5 where rownum <= 5;

Table created.

ops$tkyte@ORA920> select count(*)
2 from t;

COUNT(*)
———-
5


Отже, t – Наша таблиця з 5 рядків, в якій ми будемо виконувати одночасні зміни. msg1 – Таблиця, в яку ми будемо записувати попередні і остаточні образи рядків в тій же транзакції. У таблицю msg2 ми будемо записувати ті ж образи, але за допомогою автономної транзакції:

 ops $ tkyte @ ORA920> create or replace procedure log_msg2 (p_seq in int,
2 p_sid in int,
3 p_old_id in int,
4 p_old_y in int,
5 p_new_id in int,
6 p_new_y in int)
7 as
8 pragma autonomous_transaction;
9 begin
10 insert into msg2
11 (seq, sid, old_id, old_y, new_id, new_y)
12 values
13 (p_seq, p_sid, p_old_id, p_old_y, p_new_id, p_new_y);
14 commit;
15 end;
16 /

Procedure created.

ops $ tkyte @ ORA920> create or replace trigger t_trigger before update on t for each row
2 declare
3 l_seq number;
4 begin
5 select s.nextval into l_seq from dual;
6
7 insert into msg1
8 (seq, sid, old_id, old_y, new_id, new_y )
9 values
10 (l_seq, userenv ("sessionid"),: old.id,: old.y,: new.id,: new.y);
11
12 log_msg2
13 (l_seq, userenv ("sessionid"),: old.id,: old.y,: new.id,: new.y);
14 end;
15 /

Trigger created.


Цей тригер буде реєструвати зміни в обох таблицях – ми зможемо побачити запис в msg2 відразу, а в таблиці msg1 виявляться тільки зафіксовані запису.

ops$tkyte@ORA920> update t set y = -1 where id = 3;

1 row updated.


Встановлюємо "середньої" рядку значення -1, що, природно, її заблокує.

ops$tkyte@ORA920> set echo off

В іншому сеансі виконаємо:

update t set y = 1 where y = -1 or id = 2 or id = 3;

Потім повернемося в перший сеанс і натиснемо Enter

ops$tkyte@ORA920> pause

Я зробив це, і транзакція, природно, виявилася заблокованою. Вона змінила єдиний рядок, з id=2, І виявилася заблокованою на id=3.

 ops $ tkyte @ ORA920> select decode (msg1.seq, null, "NOT VISIBLE"), msg2 .*
2 from msg2, msg1
3 where msg2.seq = msg1.seq(+)
4 order by msg2.seq;

DECODE (MSG1 SEQ SID OLD_ID OLD_Y NEW_ID NEW_Y
———– ———- ———- ———- ——— – ———- ———-
1 12565 3 0 3 -1
NOT VISIBLE 2 12566 2 0 2 1


Можна побачити, що рядок з id=2 змінена – тригер спрацював. Транзакція чекає на рядку id=3

ops$tkyte@ORA920> set echo off

В іншому сеансі виконаємо:

update t set y = -1 where id in ( 1,5 );
commit;

ops$tkyte@ORA920> pause


Це і буде ваше "зміна першій та останній рядки". Я зробив це, і транзакція пройшла без проблем:

 ops $ tkyte @ ORA920> select decode (msg1.seq, null, "NOT VISIBLE"), msg2 .*
2 from msg2, msg1
3 where msg2.seq = msg1.seq(+)
4 order by msg2.seq;

DECODE (MSG1 SEQ SID OLD_ID OLD_Y NEW_ID NEW_Y
———– ———- ———- ———- ——— – ———- ———-
1 12565 3 0 3 -1
NOT VISIBLE 2 12566 2 0 2 1
3 12567 1 0 1 -1
4 12567 5 0 5 -1


Тепер у нашій журнальної таблиці 4 рядки. seq=1 – Це перше виконане нами зміна, і воно блокує сеанс session2. seq=2 – Це один рядок, яку вже змінив заблокований сеанс. seq = 3 / 4 – записи для зафіксованого зміни "першої / останньої" рядка.

ops$tkyte@ORA920> commit;

Commit complete.


Ось тепер все стає по-справжньому цікавим:

 ops $ tkyte @ ORA920> select decode (msg1.seq, null, "NOT VISIBLE"), msg2 .*
2 from msg2, msg1
3 where msg2.seq = msg1.seq(+)
4 order by msg2.seq;

DECODE (MSG1 SEQ SID OLD_ID OLD_Y NEW_ID NEW_Y
———– ———- ———- ———- ——— – ———- ———-
1 12565 3 0 3 -1
NOT VISIBLE 2 12566 2 0 2 1
3 12567 1 0 1 -1
4 12567 5 0 5 -1
NOT VISIBLE 5 12566 3 0 3 1
NOT VISIBLE 6 12566 1 -1 1 1
NOT VISIBLE 7 12566 2 0 2 1
NOT VISIBLE 8 12566 3 -1 3 1
NOT VISIBLE 9 12566 5 -1 5 1

9 rows selected.


Дивно – коли ми зафіксували транзакцію, перш за все, була змінена рядок з id = 3, – ми це бачили – seq=5 відповідає цій зміні. ОДНАК, вона фактично була скасована (як і зміна рядка з id=2 – Див. далі), і зміна було розпочато з початку. При цьому "повторно" були змінені рядки з id=2 і id=3, А також рядки з id 1 і 5 (вони були обрані за умовою y=-1).

ops$tkyte@ORA920> set echo off

Фіксуємо транзакцію в раніше заблокованому сеансі:

ops$tkyte@ORA920> pause
ops$tkyte@ORA920> select * from t;

ID Y
———- ———-
1 1
2 1
3 1
4 0
5 1


Отже, ясно видно, що змінені були 4 рядки і:

 ops $ tkyte @ ORA920> select decode (msg1.seq, null, "UNDONE"), msg2 .*
2 from msg2, msg1
3 where msg2.seq = msg1.seq(+)
4 order by msg2.seq;

DECODE SEQ SID OLD_ID OLD_Y NEW_ID NEW_Y
—— ———- ———- ———- ———- —- —— ———-
1 12565 3 0 3 -1
UNDONE 2 12566 2 0 2 1
3 12567 1 0 1 -1
4 12567 5 0 5 -1
UNDONE 5 12566 3 0 3 1
6 12566 1 -1 1 1
7 12566 2 0 2 1
8 12566 3 -1 3 1
9 12566 5 -1 5 1

9 rows selected.


Тепер, коли всі "зафіксовано", ми бачимо, що зміни seq=2 і seq=5 були "скасовані" – rolled back. Зроблені зміни були "скасовані", щоб можна було їх "виконати повторно" (Не хотілося б, щоб обсяг таких змін виріс в 2, 3 або N раз, чи не так?)


Отже, сервер Oracle просто змінив час виконання затроебованного вами зміни – якщо хочете, трохи змістив його.


Оператор не повинен би "перезапускатися" нескінченно, оскільки решта будуть заблоковані і будуть чекати своєї черги – вони (заблоковані оператори) залишаться заблокованими до завершення цього тривалого оператора. Міні-відкати не будуть їх розблокувати.


Якби мені потрібно було виконувати таку велику пакетне зміна, я б вважав за краще заблокувати таблицю, щоб запобігти впливу міні-транзакцій. Ми маємо справу з конфліктом методів – і, в кінцевому підсумку, більшість ваших користувачів виявляться заблокованими після декількох перезапусків тривалого оператора (тобто загальний час простою для них буде більше).


Подумайте про це – оператор повинен перезапуск, щоб "спрацювати правильно", йому дійсно необхідно бачити дані станом на "зараз", щоб безпечно змінювати їх (а що, якщо інші зміни Видалити рядки, які повинен був торкнутися оператор?)


Я б назвав вашу ситуацію небезпечною – дуже ймовірні проблеми, пов'язані з втратою змін. Блокування таблиці для такого множинного зміни здається цілком обгрунтованою або ефективною. Не хочете її встановлювати – нехай сервер робить те, що робить.


Цікаве обговорення … Коментар читача від 3 серпня 2003


Спасибі за чудову відповідь … Але що ти мав на увазі, коли писав:

"Нехай сервер робить те, що робить"

Відповідь Тома Кайта


У своїй відповіді я продемонстрував, що "робить" сервер.


Я писав:



Коментар читача від 4 серпня 2003


Добре, оскільки "шквалу коментарів" не послідувало ;-), можна я перевірю, чи правильно я зрозумів відповідь? Якщо ви проігноруєте цей довгий коментар, я зрозумію, і почекаю, поки до мене дійде черга.


a) Якщо транзакція "long" зупиниться на заблокованій рядку, вона почекає, перевірить нові значення, а потім:

 if (нові значення задовольняють конструкції where) {
виконати міні-відкат змін і повторно виконати зміна (або видалення) [випадок A]
} else {
продовжити обробку [випадок B]
}

b) У ході міні-відкоту, блокування TX транзакції "long" не знімається, так що чекають її зняття транзакції залишаються заблокованими, що допомагає запобігти "нескінченний перезапуск" транзакції "long";


c) Причина міні-відкоту і перезапуску оператора [випадок A] – в тому, що транзакція "long" побачила зміна (піддалася впливу) іншої транзакції "tx-other", але транзакція "tx-other" могла також змінити рядок, вже переглянуту транзакцією "long", тому якщо транзакцію "long" не перезапустити, вона побачить тільки частина змін транзакції "tx-other", порушуючи її неподільність;


d) Причина продовження обробки в [випадку B] – в тому, що транзакція "long" (ще) не бачила змін, виконаних транзакцією "tx-other", так що не важливо, змінила транзакція "tx-other" рядок, вже переглянуту транзакцією "long", чи ні: неподільність означає "бачити все, або нічого", а в даному випадку мова йде саме про "нічого";


e) Твердження c + d можна висловити так: "транзакція long повинна виконуватися строго після tx-other";


f) Я все одно думаю, що транзакція "long" може перезапускатися нескінченно, оскільки, наприклад, може статися наступне:



Але [це я так думаю], в ході міні-відкату не знімаються блокування вже змінених транзакцією "long" рядків, так що ймовірність спрацьовування транзакції "brand-new-tx" з часом зменшується, так що в реальності нескінченний перезапуск практично неможливий.


g) Якщо ви не проти, просто цікаво: скільки часу знадобилося на те, щоб зрозуміти моє запитання і відповісти на нього?


Відповідь Тома Кайта


a) так …


b) так …


c) Ця причина І: що, якщо рядок була змінена так, що вона більше не задовольняє умові вибору? Так, попередній образ задовольняв, а поточне значення – немає. Нехай є правило "якщо a = 5, B повинно бути в межах від 10 до 15, інакше B має дорівнювати 20 ". Ви виконуєте оператор"update t set b = 11 where a = 5". По ходу роботи ця транзакція блокується. Блокування пов'язана з тим, що інший сеанс змінює єдиний рядок -"update t set a = 6 where id = 1234". Ваше зміна не спрацює, ХОЧА ви ясно сказали" змінювати, тільки якщо a = 5 "- тому що тепер a = 6.


Так що, транзакція перезапускається і пропускає цей рядок.


d) Здається, я вас зрозумів – так.


e) Можна сказати і так


f) Я думаю, це можливо, але весьмамаловероятно.


g) Читання питання зажадало більше часу, ніж відповідь на нього. Було складно розібратися, що ви маєте на увазі, але цікаво. Можливо, я зроблю це питання темою окремої презентації. Мене здивувала відсутність коментарів – я думав отримати багато повідомлень типу "Ось воно як …" 😉


Чому Rollback і Commit обробляються по-різному?


У ході вивчення цього обговорення мене здивувало, чому сервер Oracle працює по-різному, коли блокуючий сеанс виконує відкат замість фіксації. Звичайно, дані заблокованою рядки при відкат не змінюються, але, мені здається, було б більш логічно (для одноманітності), якщо б відкат змін заблокованою рядки теж викликав би міні-відкат транзакції LONG. У чому я неправий?


Відповідь Тома Кайта


Все питання в тому, чи змінилися дані. Якщо у вихідному прикладі замінити:

update t set y = -1 where id = 3;

на

update t set y = y where id = 3;

Блокування будуть тими ж. Сеанс 1 заблокує рядок з id = 3. Сеанс 2 змінить рядок з id = 2, а потім виявиться заблокованим на id = 3. Сеанс 3 потім змінить рядок з id = 1 і id = 5, і зафіксує зміни. Потім сеанс 1 фіксує транзакцію – сеанс 2 виявиться розблокованим і виявить "ага, значення y не змінилося – не про що турбуватися, можна продовжувати роботу …", і змінить у результаті тільки два рядки.


Ось сценарій, який я використав, без жодних запрошень SQL * Plus, і він показує, що навіть при фіксації, якщо значення не змінилося, робота продовжиться як у випадку відкоту.


Більш-менш узгодженим результат не буде – він виявиться "настільки" ж узгодженим, але продуктивність сущесвенно підвищиться …

drop table msg1;
drop table msg2;
drop sequence s;
drop table t;
set echo on;

create sequence s;

create table msg1
( seq int primary key, sid int,
old_id int, old_y int,
new_id int, new_y int );

create table msg2
( seq int primary key, sid int,
old_id int, old_y int,
new_id int, new_y int );

create table t
as
select rownum id, 0 y
from all_users
where rownum <= 5;

select count(*)
from t;

create or replace procedure log_msg2( p_seq in int,
p_sid in int,
p_old_id in int,
p_old_y in int,
p_new_id in int,
p_new_y in int )
as
pragma autonomous_transaction;
begin
insert into msg2
(seq, sid, old_id, old_y, new_id, new_y )
values
(p_seq, p_sid, p_old_id, p_old_y, p_new_id, p_new_y );
commit;
end;
/

create or replace trigger t_trigger before update on t for each row
declare
l_seq number;
begin
select s.nextval into l_seq from dual;
insert into msg1
(seq, sid, old_id, old_y, new_id, new_y )
values
(L_seq, userenv ("sessionid"),: old.id,: old.y,: new.id,: new.y);
log_msg2
(L_seq, userenv ("sessionid"),: old.id,: old.y,: new.id,: new.y);
end;
/

update t set y = y where id = 3;
set echo off
prompt В іншому сеансі виконайте:
prompt update t set y = 1 where y = -1 or id = 2 or id = 3;;
prompt Потім поверніться в цей та натисніть enter
pause
select decode(msg1.seq,null,”NOT VISIBLE”), msg2.*
from msg2, msg1
where msg2.seq = msg1.seq(+)
order by msg2.seq;
set echo off
prompt В іншому сеансі виконайте:
prompt update t set y = -1 where id in ( 1,5 );;
prompt commit;;
pause
select decode(msg1.seq,null,”NOT VISIBLE”), msg2.*
from msg2, msg1
where msg2.seq = msg1.seq(+)
order by msg2.seq;
commit;
select decode(msg1.seq,null,”NOT VISIBLE”), msg2.*
from msg2, msg1
where msg2.seq = msg1.seq(+)
order by msg2.seq;
set echo off
prompt Виконайте commit в раніше заблокованому сеансі
pause
select * from t;
select decode(msg1.seq,null,”UNDONE”), msg2.*
from msg2, msg1
where msg2.seq = msg1.seq(+)
order by msg2.seq;


Буде потрібно близько 6 місяців …


Мені здається, буде потрібно не менше шести місяців, щоб я це зрозумів. Я завжди говорив, що однією з сильних сторін сервера Oracle є можливість зрозуміти, що відбувається "за кадром", і це – відмінний приклад.


Відповідь Тома Кайта


Ось ще цікаві варіанти:


a) Якщо замінити update t set y = -1 where id =3; на update t set y = y where id = 3;


то замість 4 будуть змінені тільки 2 рядки …


b) Додайте в таблицю t стовпець Z з нульовими значеннями у всіх рядках. Виконайте перша зміна:
update t set y = y, z = 1 where id = 3;


Тепер подивіться, скільки рядків буде змінено (спробуйте здогадатися)


c) Залишивши стовпець Z з нулями, додайте наступний тригер:

create or replace trigger t after update on t for each row
begin
dbms_output.put_line(:new.z);
end;
/

і використовуйте наступний оператор update:

   update t set y = y, z = 1 where id = 3;

Тепер подивіться, скільки рядків було змінено …


Цікава різновид цікавого варіанту



a) Якщо замінити update t set y = -1 where id =3; на update t set y = y where id = 3;


то замість 4 будуть змінені тільки 2 рядки …


Так! Тому що оператор "update t set y = 1 where y = -1 or id = 2 or id = 3"(Який я позначив" long ") не був перезапущений, тому він і не побачив зміни на -1, виконаного транзакцією" first / last ". Тобто він був узгоджений як образ до" first / last ".


Ось варіант вашого сценарію, в якому для реєстрації використовується client_info, І його остаточні результати:

drop table msg1;
drop table msg2;
drop sequence s;
drop table t;
set echo on
set lines 123
col client_info format a15
set pages 9999
create sequence s;
create table msg1
( seq int primary key, sid int,
old_id int, old_y int,
new_id int, new_y int );
create table msg2
( seq int primary key, sid int,
old_id int, old_y int,
new_id int, new_y int,
client_info varchar2(64) );
create table t
as
select rownum id, 0 y
from all_users
where rownum <= 5;
select count(*)
from t;
create or replace procedure log_msg2( p_seq in int,
p_sid in int,
p_old_id in int,
p_old_y in int,
p_new_id in int,
p_new_y in int )
as
pragma autonomous_transaction;
client_info varchar2(64);
begin
dbms_application_info.read_client_info (client_info);
insert into msg2
(seq, sid, old_id, old_y, new_id, new_y, client_info )
values
(P_seq, p_sid, p_old_id, p_old_y, p_new_id, p_new_y, client_info);
commit;
end;
/
show errors;
create or replace trigger t_trigger before update on t for each row
declare
l_seq number;

begin
select s.nextval into l_seq from dual;
insert into msg1
(seq, sid, old_id, old_y, new_id, new_y )
values
(L_seq, userenv ("sessionid"),: old.id,: old.y,: new.id,: new.y);
log_msg2
(L_seq, userenv ("sessionid"),: old.id,: old.y,: new.id,: new.y);
end;
/
exec dbms_application_info.set_client_info (“blocker”);
update t set y = y where id = 3;

— update t set y = -1 where id = 3;

set echo off
prompt В іншому сеансі виконайте:
prompt exec dbms_application_info.set_client_info (“long”);;
prompt update t set y = 1 where y = -1 or id = 2 or id = 3;;
prompt Потім поверніться в цей та натисніть enter
pause
select decode(msg1.seq,null,”NOT VISIBLE”), msg2.*
from msg2, msg1
where msg2.seq = msg1.seq(+)
order by msg2.seq;
set echo off
prompt В іншому сеансі виконайте:
prompt exec dbms_application_info.set_client_info ("first / last");;
prompt update t set y = -1 where id in ( 1,5 );;
prompt commit;;
prompt Потім поверніться в цей та натисніть enter
pause
select decode(msg1.seq,null,”NOT VISIBLE”), msg2.*
from msg2, msg1
where msg2.seq = msg1.seq(+)
order by msg2.seq;
commit;
select decode(msg1.seq,null,”NOT VISIBLE”), msg2.*
from msg2, msg1
where msg2.seq = msg1.seq(+)
order by msg2.seq;
set echo off
prompt Зафіксуйте раніше заблокований сеанс (long)
prompt потім поверніться в цей та натисніть enter
pause
select * from t;
select decode(msg1.seq,null,”UNDONE”), msg2.*
from msg2, msg1
where msg2.seq = msg1.seq(+)
order by msg2.seq;


Остаточний результат:

        ID          Y
———- ———-
1 -1
2 1
3 1
4 0
5 -1

DECODE (MSG1.SEQ, NU SEQ SID OLD_ID OLD_Y NEW_ID
NEW_Y CLIENT_INFO
—————— ———- ———- ———- – ——– ———-
———- —————
1 3967 3 0 3
0 blocker
2 3968 2 0 2
1 long
3 3969 1 0 1
-1 first/last
4 3969 5 0 5
-1 first/last
5 3968 3 0 3
1 long


А тепер інша цікава різновид цікавого варіанти: що якщо рядок з id = 5 задовольняє конструкції where з самого початку, а потім вона змінюється транзакцією "first / last" так, що перестає задовольняти умові (я вибрав значення 99)?


Давайте почнемо з такого стану таблиці:

        ID          Y
———- ———-
1 0
2 0
3 0
4 0
5 -1

Ось що ми будемо робити:

drop table msg1;
drop table msg2;
drop sequence s;
drop table t;
set echo on
set lines 123
col client_info format a15
set pages 9999
create sequence s;
create table msg1
( seq int primary key, sid int,
old_id int, old_y int,
new_id int, new_y int );
create table msg2
( seq int primary key, sid int,
old_id int, old_y int,
new_id int, new_y int,
client_info varchar2(64) );
create table t
as
select rownum id, 0 y
from all_users
where rownum <= 5;
update t set y = -1 where id = 5;
select * from t;
create or replace procedure log_msg2( p_seq in int,
p_sid in int,
p_old_id in int,
p_old_y in int,
p_new_id in int,
p_new_y in int )
as
pragma autonomous_transaction;
client_info varchar2(64);
begin
dbms_application_info.read_client_info (client_info);
insert into msg2
(seq, sid, old_id, old_y, new_id, new_y, client_info )
values
(P_seq, p_sid, p_old_id, p_old_y, p_new_id, p_new_y, client_info);
commit;
end;
/
create or replace trigger t_trigger before update on t for each row
declare
l_seq number;

begin
select s.nextval into l_seq from dual;
insert into msg1
(seq, sid, old_id, old_y, new_id, new_y )
values
(L_seq, userenv ("sessionid"),: old.id,: old.y,: new.id,: new.y);
log_msg2
(L_seq, userenv ("sessionid"),: old.id,: old.y,: new.id,: new.y);
end;
/
exec dbms_application_info.set_client_info (“blocker”);

— update t set y = -1 where id = 3;

update t set y = y where id = 3;
set echo off
prompt В іншому сеансі виконайте:
prompt exec dbms_application_info.set_client_info (“long”);;
prompt update t set y = 1 where y = -1 or id = 2 or id = 3;;
prompt Потім поверніться в цей та натисніть enter
pause
select decode(msg1.seq,null,”NOT VISIBLE”), msg2.*
from msg2, msg1
where msg2.seq = msg1.seq(+)
order by msg2.seq;
set echo off
prompt В іншому сеансі виконайте:
prompt exec dbms_application_info.set_client_info ("first / last");;
prompt update t set y = 99 where id in ( 1,5 );;
prompt commit;;
prompt Потім поверніться в цей та натисніть enter
pause
select decode(msg1.seq,null,”NOT VISIBLE”), msg2.*
from msg2, msg1
where msg2.seq = msg1.seq(+)
order by msg2.seq;
commit;
select decode(msg1.seq,null,”NOT VISIBLE”), msg2.*
from msg2, msg1
where msg2.seq = msg1.seq(+)
order by msg2.seq;
set echo off
prompt Зафіксуйте транзакцію в раніше заблокованому сеансі (long)
prompt Потім поверніться в цей та натисніть enter
pause
select * from t;
select decode(msg1.seq,null,”UNDONE”), msg2.*
from msg2, msg1
where msg2.seq = msg1.seq(+)
order by msg2.seq;


Остаточний результат:

        ID          Y
———- ———-
1 99
2 1
3 1
4 0
5 99

DECODE (MSG1.SEQ, NU SEQ SID OLD_ID OLD_Y NEW_ID
NEW_Y CLIENT_INFO
—————— ———- ———- ———- – ——– ———-
———- —————
1 3971 3 0 3
0 blocker
UNDONE 2 3968 2 0 2
1 long
3 3969 1 0 1
99 first/last
4 3969 5 -1 5
99 first/last
UNDONE травня 3968 3 0 3
1 long
UNDONE 6 3968 5 -1 5
1 long
7 3968 2 0 2
1 long
8 3968 3 0 3
1 long


Оператор seq=6 виконав зміна (я думаю, old_y=-1 залишилося від моментального знімка, який береться на початку виконання оператора), але потім транзакція "long" побачила, що рядок була змінена, тому оператор був перезапущений.


Ви згодні?


Скільки брудних буферів? Коментар читача від 19 серпня 2003


Якщо при кожній зміні береться узгоджений образ блоку, то для кожного блоку має бути кілька брудних буферів. Як ці зміни об'єднуються – в кінцевому підсумку, на диск адже пишеться один блок? (Якщо записувати на диск всі брудні образи, на диску не виявляться всі зміни.)


Відповідь Тома Кайта


Є тільки один брудний буфер, останній. Решта – просто погоджені з читання версії того ж блоку на різні моменти часу.


Єдино вірною є остання версія блоку.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*