quinta-feira, 18 de novembro de 2010

Transação Autônoma

Toda transação dentro do contexto de Sistema Gerenciador de Banco de Dados (SGBD) trabalha com o conceito de Atomicidade, ou seja, a transação é indivisível. Isso significa que, ou ela ocorre inteira ou não ocorre. A linguagem de programação PL/SQL da Oracle possui uma extensão não muito conhecida, Transação Autônoma, mas bem interessante e que pode ser muito útil para resolver alguns casos. Este artigo detalha a explicação teórica deste recurso, para tanto será apresentado um exemplo real de como este recurso resolveu o problema e por fim serão demonstrados alguns casos nos quais não se deve aplicar este recurso.

Fundamentação Teórica

Uma Transação Autônoma é uma transação que é inicializada por outra transação, e opera de modo independente da transação que a invocou. A transação autônoma sempre deve terminar antes de devolver o controle do processo para transação que a invocou. E se a transação autônoma terminar com commit os dados serão gravados no banco de dados ainda que e a transação principal falhe e realize um rollback.

Os programas que usam transação autônoma, também são conhecidos como programas cujo escopo opera sobre transações múltiplas. Isso aumenta a complexidade dos programas, o que implica em ter sempre certeza dos benefícios e conhecer os riscos de se trabalhar com múltiplas transações.

Os objetos seqüências do Oracle se comportam como uma transação autônoma. Ao chamar o método nextval de uma seqüência, a mesma sempre irá para o próximo valor independente do sucesso ou fracasso da transação que chamou o nextval.

Deve ficar claro que uma transação autônoma não sabe nada da transação que a invocou, então atualizações feitas dentro de um contexto de transação não serão visíveis pela transação autônoma. Caso uma transação faça inserção em um registro, e logo depois invoque uma transação autônoma e esta tente consultar o novo registro, a exceção no_data_found será lançada. Pois o novo registro está disponível apenas para primeira transação até que seja aplicado o commit na mesma.

Dentro da PL/SQL, o comportamento padrão é que os procedimentos participem de uma mesma transação, ou seja, se o procedimento "A" chamar o procedimento "B" ambos participarão da mesma sessão de transação. Logo para o sucesso da transação, os dois procedimentos devem ser executados com êxito.

Para que o procedimento "B" mude este comportamento deve ser usado o Pragma AUTONOMOUS_TRANSACTION, que sinaliza para o compilador que o procedimento “B” não irá participar da transação do procedimento “A”. O procedimento “B” operará sobre o escopo de uma nova transação, que é a transação autônoma, e antes do procedimento “B” devolver o controle para o procedimento “A”, ele deve terminar a transação.

A sintaxe é a seguinte:

create or replace procedure proc_au is

Pragma Autonomous_Transaction;

begin

<<>>

Commit;

end;

Devido a essa característica, a transação autônoma é muito usada para geração de log's. Pois para rastrear o que causou a falha em uma transação é preciso reconstruir o que a transação fez. Entretanto, se os log’s foram gerados dentro do contexto da transação que falhou, o rollback irá desfazer tudo que a transação fez inclusive os log’s. Por isso o ideal é que os log's sejam gerados a partir de uma transação autônoma.

Para criação do Log, é preciso a criação de uma tabela, que armazenará os erros decorrentes nos processos de negocio.

CREATE TABLE Log_Error

( error_id number(9) CONSTRAINT PK_error PRIMARY KEY,

processo_nome varchar2(255),

error_code number(9),

sqlerror_message varchar2(2000) ,

user_error varchar2(30),

date_error date);

create sequence SEGLog_Error start with 1

increment by 1 nocache;

Após a criação da tabela, a inserção nesta tabela do log de erro deve ser feita por um procedimento autônomo.

create procedure InsereLog(p_processo_nome Log_Error. processo_nome%type) is

Pragma Autonomous_Transaction;

v_sqlcode number;

v_sqlerrm varchar2(4000);

begin

v_sqlerrm := sqlerrm;

v_sqlcode := sqlcode;

insert into Log_Error (error_id, processo_nome, error_code, sqlerror_message, user_error, date_error)

values (seglog_error.nextval, p_processo_nome, v_sqlcode, v_sqlerrm, user,sysdate);

Commit;

end InsereLog;

/

Então esta procedure pode ser invocada dentro do controle de manipulação de exceção de um bloco PL/SQL, podendo tanto ser um bloco declarado (um objeto compilado no banco de dados) ou um bloco anônimo.

No exemplo que se segue, um bloco PL/SQL anônimo tenta executar uma transação que insere NULL em uma tabela que aceita NULL e depois tenta inserir NULL para uma tabela que não aceita NULL, ocorrendo o erro “ORA-01400” no segundo insert.

Create table TabAceitaNULL

(CampoNULL number null);

Create table TabNaoAceitaNULL

(CampoNaoNULL number not null);

begin

insert into TabAceitaNULL (CampoNULL) values (NULL);

insert into TabNaoAceitaNULL (CampoNaoNULL) values (NULL);

exception

when OTHERS then

InsereLog(‘Erro ao inserir nas tabelas TabAceitaNULL e TabNaoAceitaNULL’);

raise;

end;

/

Ao executar o bloco anônimo supracitado, a transação falhará na segunda instrução insert, ocorrerá então um rollback, que desfará tanto o segundo como o primeiro insert. Contudo o LOG não será desfeito, pois não participou da transação principal na qual ocorreu o erro.

Ao realizar consulta sobre as tabelas TabAceitaNULL e TabNaoAceitaNULL estarão vazias, e ao consultar a tabela Log_Error é possível ver os log’s dos erros.

Outro ponto a ser comentado é que comumente, os desenvolvedores utilizam procedimentos para realizar atualizações no banco de dados, e as funções ficam limitadas a consultas ou cálculos. Isso porque os procedimentos eram a única forma de aplicar alterações no banco de dados. A partir da versão Oracle 10g funções podem realizar alterações no banco de dados e sendo elas autônomas ou não.

Contudo as funções não autônomas que realizam operações dentro do banco de dados ainda não poderão ser chamadas a partir de uma instrução de SELECT, caso isso seja feito irá ocorrer o erro “ORA-14552”. Contudo se a função abrir uma transação autônoma, ela poderá ser usada em instrução SELECT. Este comportamento é exatamente igual aos objetos seqüências.


Cenário do problema e resolução


Empresas que tem um grande volume de boletos a serem pagos pelos clientes usam a troca de informações de cobrança em rede bancaria. E seus sistemas devem gerar boletos para que os clientes possam pagar o título na rede bancaria. Estes boletos devem conter o "nosso número", informação que a rede bancaria usa para identificar quais boletos foram liquidados.

Em geral o “nosso número”, que os estabelecimentos bancários usam, é um número dentro de uma faixa numérica. O valor da faixa numérica pode variar de banco para banco, além disso, existe o digito verificador que é baseado no calculo do Módulo 11 e entre outros detalhes. Contudo apenas para simplificar e a titulo de ilustração, a faixa numérica seria do número 1 a 9999999 pelo banco "A".

Então em uma empresa que possui várias filiais, cada filial tem a sua carteira de cobrança e por sua vez gera os boletos. Para cada boleto gerado, o "nosso numero" tem que ter o valor numérico igual ao do boleto antecessor acrescido de um, então o analista criou uma tabela para guardar o valor atual da carteira de cobrança:

create table carteira_cobranca

( codbanco integer not null,

codcedente integer not null,

nossonumero integer not null);

Cada linha da tabela seria uma carteira de cobrança que guarda o código do banco, o código cedente e o valor atual do “nosso número”, o analista pensou assim para não precisar criar seqüências no Oracle. Isso porque toda vez que se cria uma carteira nova, uma nova seqüência precisaria ser criada, solicitando ao DBA a criação da mesma.

Se uma empresa tem, por exemplo, 50 filiais e trabalha com 5 bancos diferentes então teríamos 250 carteiras. E se esta empresa desejasse trabalhar com algum outro banco, mais 50 carteiras de cobrança seriam necessárias. Este fato inviabiliza a manutenção (uma vez que é faixa numérica e o nosso numero precisa ser reiniciado) e a nomeação das seqüências.

Uma vez criada a tabela, em seqüência cria-se uma função PL/SQL que retorna o nosso numero, um código simplificado segue abaixo:

create function nosso_numero(p_codbanco in carteira_cobranca.codbanco%type,

p_codcedente in carteira_cobranca.codcedente%type)

return number is

v_nossonumero carteira_cobranca.nossonumero%type;

begin

-- Altera o nosso numero e retorna o sequencial...

update carteira_cobranca c

set c.nossonumero = nossonumero + 1

where c.codbanco = p_codbanco

and c.codcedente = p_codcedente

returning c.nossonumero into v_nossonumero;

return v_nossonumero;
end;

/

Vale lembrar que em um sistema real, essa função precisaria checar se o nosso número não chegou ao valor máximo disponível da faixa, e reiniciar a contagem, mas isso foi suprimido, pois o foco deste artigo é o bloqueio que ocorre da instrução UPDATE.

O problema surge quando o departamento financeiro precisa gerar um lote de boletos para uma determinada carteira, e então enquanto não terminar o processamento do lote com um Commit ou Rollback, os operadores de frente de caixa não conseguem gerar novos boletos desta mesma carteira. Devido ao bloqueio que ocorre na função nosso_numero que impede alterações no registro da carteira de cobrança.

Este é o comportamento padrão no controle de concorrência de banco de dados, para que o banco de dados garanta o I do ACID, que é o Isolamento de transações, o SGBD usa o recurso de bloqueio sobre a linha alterada e as transações ocorrem seqüenciais sobre esta linha. Primeiro é executada uma transação e só depois do termino da primeira é executada a segunda.

E é exatamente nestas situações que a transação autônoma pode ser uma excelente ferramenta. O bloqueio na tabela carteira cobrança iria somente ocorrer dentro da função nosso_numero e não dentro da transação de geração em lote dos boletos.

A função seria reescrita da forma abaixo:

create or replace function nosso_numero(p_codbanco in carteira_cobranca.codbanco%type,

p_codcedente in carteira_cobranca.codcedente%type)

return number is
Pragma Autonomous_Transaction;

v_nossonumero carteira_cobranca.nossonumero%type;

begin

-- Altera o nosso numero e retorna o sequencial...

update carteira_cobranca c

set c.nossonumero = nossonumero + 1

where c.codbanco = p_codbanco

and c.codcedente = p_codcedente

returning c.nossonumero into v_nossonumero;

commit;

return v_nossonumero;
end;

/

Nesse exemplo se o departamento financeiro fosse gerar um lote com 1000 boletos e após gerar uns 5 boletos, sendo o último com o nosso número igual a 150 e neste instante algum operador do caixa gerasse um boleto, então o operador de caixa iria gerar o boleto com nosso número 151 e o 6 boleto gerado pelo lote do departamento financeiro iria gerar o nosso número 152. Com isso teríamos o comportamento bem parecido com os objetos seqüência do Oracle, melhorando assim o controle de concorrência e evitando bloqueios (locks).

Quando não usar uma transação Autônoma

Como explicado uma transação Autônoma, é uma transação que não participa da transação do procedimento que a invocou. Em um bom projeto de software uma grande tarefa tem suas responsabilidades divididas por vários objetos e seus métodos.

Fazendo uma co-relação que classes de objetos sendo pacotes do PLSQL e os métodos sendo funções e procedimentos, então em uma grande tarefa, vários procedimentos de vários pacotes poderiam ser invocados para realizar essa tarefa que por sua vez é atômica.

Considere o caso de um livro-razão contábil. Em termos contábeis, é impossível fazer este processamento parte em um período e parte em outro, de modo que o processamento em bloco a cada fim do mês é uma transação de negócio indivisível. Caso algum procedimento/função for autônoma ela irar gravar sua operação no banco de dados mesmo se a tarefa como um todo falhar. Então teríamos uma inconsistência de informação.

Outra situação na qual não se deve usar transação Autônoma é em trigger para resolver problema de tabela mutante. Em fóruns o consenso é que o problema seja resolvido adicionando o Pragma Autonomous_Transaction .

Usar este pragma em trigger também quebra o conceito de atomicidade da transação, pois todo operação feita dentro da trigger também irá ser gravado no banco de dados mesmo se a transação como um todo falhe.

Considerações finais

Toda ferramenta antes de ser usada deve ser conhecida, a transação autônoma não é exceção a esta regra. A Transação autônoma pode resolver problemas reais, contudo se usada de forma indiscriminada pode causar problemas.

Neste artigo, foi apresentado até um certo nível de detalhe do comportamento de transação autônoma, contudo detalhes mais profundos sobre o assunto e exemplos sobre este recurso, podem ser encontrados no livro “Oracle Database 11g PL/SQL Programming”.

Um comentário:

  1. Olá Alessandro,

    Tudo bem! Poderia entrar em contato comigo pelo e-mail contato@rodrigoalmeida.net, pois gostaria de convidar-lo para o GUOB TECH DAY 2011.

    Abraços,
    Rodrigo Almeida

    ResponderExcluir