Не сме сами, част 3

 Общи  Коментарите са изключени за Не сме сами, част 3
Окт. 282009
 

(към част 2)

Един малко по-добър вариант изглежда така:

– В началото вземаме текущата дата
– Когато избираме кои данни да обработим, освен processed=0 гледаме и датата на зареждане – да е по-малка от датата за начало на обработката, която се запомнили
– когато изтриваме обработените данни от loader таблицата, отново изтриваме само тези, които са „заредени“ преди началото на обработката

Процедурата би изглеждала така:

create or replace procedure Process_New_Data2 is
  -- example code - has a BUG!!!
  transfer_date date;
begin
  -- get the time when we start the transfer
  transfer_date := sysdate;

  -- process the loaded batches
  insert /*+append*/
  into prod_table
    select -- do some processing
     batch_id, get_something(column1, column2), calculate_something(column3, column4),
     RANK() over(PARTITION BY column5, column6 order by column7) rn, 
     column8, column9, ...
      from loader_table
     where batch_id in (select batch_id
                          from batch_log
                         where load_time < transfer_date -- get only the batches loaded before we start
                           and processed = 0);

  -- delete the rows from loader table
  delete from loader_table
   where batch_id in (select batch_id
                        from batch_log
                       where load_time < transfer_date -- delete only the batches loaded before we start
                         and processed = 0);

  -- mark the batches as processed
  update batch_log
     set processed = 1
   where load_time < transfer_date -- mark only the batches loaded before we start
     and processed = 0;

  commit;
end;

За съжаление това отново не е гарантирано решение. Шанса за загуба на данни тук е много по-малък, но не е нулев, и много зависи от метода на зареждане на нови данни.

Проблема се изразявана едно ниво по-дълбоко: някоя сесия може да е заредила данни преди няколко секунди (може и минути), но да не е commit-нала промените. Съответно нашият inser-select няма да ги хване, но докато върви той, другата сесия има достатъчно време да направи commit и няколко милиона реда да се появят за малко, докато delete-а не ги изтрие безславно.

(към част 4)

 Posted by at 13:55

Не сме сами, част 2

 Общи  Коментарите са изключени за Не сме сами, част 2
Окт. 282009
 

(към част 1)

Дублирането на данни е много неприятно. То може да доведе до всякакви грешни резултати. Това, че се случва произволно, го направи и трудно за елиминиране (ако няма някакъв естествен ключ в данните). Така приходите на фирмата може да излязат 120 000, вместо 115 000. По-лошо: това може да се случи и с разходите 🙂

Но много по-тежка е ситуацията със загубата на данни. Това е в общи линии най-лошото, което може да се случи на една информационна система. Ако работи бавно – това се оправя (викате ме и ви казвам как 😉 ), ако не е удобна за потребителите – също, ако не си върши работата – доработва се. Но ако има загуба на данни, оправията е много по-трудна, понякога и невъзможна.

Ето и една весела процедура, която освен до дублиране, може до доведе и до загуба на данни:

create or replace procedure Process_New_Data is
  -- example code - has a BUG!!!
begin
  -- process the loaded batches
  insert /*+append*/
  into prod_table
    select -- do some processing
           batch_id, get_something(column1, column2), calculate_something(column3, column4),
           RANK() over(PARTITION BY column5, column6 order by column7) rn, 
           column8, column9, ...
      from loader_table
     where batch_id in (select batch_id
                          from batch_log
                         where processed = 0);

  -- delete the rows from loader table
  delete from loader_table
   where batch_id in (select batch_id
                        from batch_log
                       where processed = 0);

  -- mark the batches as processed
  update batch_log
     set processed = 1
   where processed = 0;

  commit;
end;

Тук забавлението настава така:
Сесия 1 прехвърля (много) данни с insert-select. Това, естествено, отнема време
Сесия 2 зарежда още един (или няколко няколко) batch в loader таблицата. Естествено, те са с processed = 0
Сесия 1 завършва с прехвърлянето и изтрива всички редове, които са били маркирани като не-прехвърлени (включително и новопостъпилите)

В този случай няма никакво значение къде ще сложите commit. И след всеки ред да има commit, пак няма да се избегне загубата на данни.

(към част 3)

 Posted by at 10:06

Не сме сами, част 1

 Общи  Коментарите са изключени за Не сме сами, част 1
Окт. 282009
 

(към част 0)

Първият пример е за система, в която постъпват данни от много места (асинхронно) и се правят се различни обработки (асинхронно). По всяко време някой процес може да извика дадена процедура и понякога това може да предизвика конфликти, които да доведат до дублиране (по-добрия вариант) или загуба на информация.

Ето примерна процедура, която води до дублиране на информация:

create or replace procedure Move_Some_Data is
  -- example code - has a BUG!!!
begin
  -- find all non-archived batches
  for batches in (select batch_id
                    from batch_log
                   where archived = 0) loop
    -- archive the current batch
    insert /*+append*/
    into arch_table
      select *
        from prod_table
       where batch_id = batches.batch_id;
  
    -- delete the rows from prod table
    delete from prod_table
     where batch_id = batches.batch_id;

  ... -- other tables containing batch information
  
    -- mark the batch as archived
    update batch_log
       set archived = 1
     where batch_id = batches.batch_id;
  
    -- finish the transaction
    commit;
  end loop;
end;

В този случай, ако по някаква грешка пуснем процедурата 2 пъти (почти) едновременно, има голяма шанс едни и същи данни да се случат в архива 2 пъти. Ето един интересен вариант:

– Сесия 1 изпълнява select заявката и получава курсор към не-архивираните batches 89, 90, 91 и 92. Започва цикъл за прехвърляне.
– Сесия 1 минава една итерация и прехвърля batch 89. Започва insert – select от втора итерация за batch 90
– Сесия 2 се включва в този момент. Изпълнява бързия select и получава курсор към не-архивираните (към този момент) batches 90, 91 и 92. Започва цикъл за прехвърляне.
– Сесия 2 също започва insert – select за прехвърляне на batch 90
– Сесия 1 завършва прехвърлянето на batch 90 и изтрива данните. Това не пречи по никакъв начин на сесия 2, защото тя получава consistent read копие на данните от преди изтриването
– Нататък може да стават всякакви произволни комбинации. Примерно ако данните в batch 91 са значително по-малко от тези в batch 90, сесия 1 може да ги прехвърли и изтрие преди сесия 2 да е свършила с първата си итерация. В този случай когато Сесия 2 стигне до втората си итерация, ще направи insert на 0 реда (и съответно delete), което би било много забавно за наблюдение ако има организиран log.

Елементарната защита в такъв случай би била вместо select да правим select for update на таблица batch_log. Това, в случая, не върши работа, защото (съвсем коректно), след всяка прехвърлена група записи се прави commit.

Едно по-добро решение ще предложа малко по-късно.

(към част 2)

 Posted by at 9:07
Окт. 282009
 

Преди 10 години, когато още бях програмист ( 😉 ), най-яката книга за мен беше Advanced Windows на Jeffrey Richter. Там се обясняват на дълго и на широко „вътрешностите“ на Windows от гледа точка на програмиране. Най-якото за мен беше multi-threaded програмирането и си умирах от кеф да правя някакви неща на много нишки.

При multi-threaded програмирането, основният проблем е синхронизацията между нишките. За да стане добре, програмиста трябва да може да мисли многонишково; да идентифицира критичните секции, евентуалните конфликтни места, и да ги защити. Основно правило е, че текущата нишка може да бъде прекъсната във всеки един момент, между всеки 2 инструкции, и ако работи с глобални данни, да настанат бели. Това важи и на ниво процес, но там има много по-голям шанс да ти се размине, защото обикновено ресурсите са ти доста локални. Да не задълбавам….

При многопотребителските БД имаме същото предизвикателство. За съжаление много програмисти не мислят за това. Някои мислят само за performance. Други и за това не мислят, но пак се отклонявам.

Ще се опитам да направя една серия, в която да покажа такива опасни практики. Честно казано, в момента не ми се занимава да пиша учебникарски примери за левашко управление на транзакции, макар че и такива съм виждал. Ще покажа варианти, които на пръв поглед изглеждат съвсем правилно написани (за нетренирано око). Ще използвам псевдо-код, написан на PL/SQL; но същите принципи важат и на Java, C#, Perl и кой да е друг език.

Между другото, коварното при такива бъгове е, че се проявяват много късно (когато системата се натовари с повечеко потребители). При „стандартно“ тестване всичко си минава – входните данни се преобразуват в изходни. Още по-коварно е, че се проявяват произволно във времето, сякаш напълно случайно, и е адски трудно да се reproduce-нат.

Stay Tuned!

(към част 1)

 Posted by at 8:54