Documento: Guia de boas práticas - Transações


 

Quando se  trabalha num ambiente multiusuário, se faz necessário um controle de concorrência das atualizações realizadas no banco de dados. Controle de concorrência é um método usado para garantir que as transações sejam executadas de forma segura e que sigam as regras de Atomicidade, Consistência, Isolamento e Durabilidade. Uma transação é uma unidade que preserva consistência.É necessário, portanto, que qualquer escalonamento produzido ao se processar um conjunto de transações concorrentemente seja computacionalmente equivalente a um escalonamento executando essas transações serialmente em alguma ordem. Diz-se que um sistema que garante esta propriedade assegura a seriabilidade. A serialização das operações reduz o desempenho da aplicação, principalmente quando realizada em tabelas de alta concorrência.
A linha Microsiga Protheus dispõe de um padrão para as operações de concorrência, que visam reduzir as ocorrênicas da serialização da aplicação, estas operações podem ser dividas em:
·         Leitura
·         Bloqueio de Interface
·         Bloqueio de processamento
·         Bloqueio de transação
Leitura
A linha Microsiga Protheus adota o nível de isolamento READ UNCOMMITTED. Este nível de isolamento permite leituras sujas, onde a transação corrente ou as demais podem visualizar os dados das transações em aberto.
O nível de isolamento adotado garante que as leituras realizadas no banco de dados pela aplicação não realizem bloqueio de registros e por conseqüência não serializem a aplicação, mantendo a previsibilidade do desempenho. Porém alguns cuidados devem ser observados para garantir a integridade dos dados.
Quando se adota leituras sujas deve-se prevenir que em atualizações, os dados sejam bloqueados antes de serem atualizados ou lidos com este fim.  A transação T2 tem uma linha alterada, mas ainda não finalizada. A transação T1 lê os dados atualizados e os atualiza com base nos dados lidos. A transação T1 fecha a transação e a transação T2 realiza um Roll Back dos dados. A partir deste momento os dados não estão mais íntegros.
Para evitar esta situação, o desenvolvedor deve indicar a aplicação o momento do bloqueio, isto é feito utilizando a função RecLock, conforme demonstrado abaixo:
Begin Transaction
RecLock(“SB2”)
nQtd := SB2->B2_QATU
If nQtd + nSoma <= 0
                SB2->B2_QATU := 0
Else
                SB2->B2_QATU := nQtd - nSoma
EndIf
MsUnLock()
End Transaction
 
A função RecLock informa para o Framework da aplicação realizar o bloqueio do registro posicionado. O Framework realiza o bloqueio e atualiza a leitura do registro corrente da tabela. As demais transações do sistema continuam lendo o registro bloqueado, porém se tentarem bloquear o registro deverão aguardar a transação anterior liberá-lo, o que somente pode ser realizado ao termino da transação.
Outra prevenção a ser considerada nas leituras sujas são as atualizações parciais, decorrentes do relacionamento de dependência direta das tabelas. Para um formulário do tipo Master/Detail (cabeçalho e Item), a transação de inserção deste formulário permite que o registro da tabela Master exista sem os registros ou com uma quantidade parcial dos registros da tabela Detail, haja vista que é a única forma de garantir a dependência da chave estrangeira. Se uma rotina de atualização realizar o bloqueio apenas nos registros Detail não há como garantir que todos os registros sejam considerados. Portanto é necessário bloquear o registro Master, antes do inicio do bloqueio dos registros Detail em qualquer atualização dos registros Detail. Ao bloquear o registro Master, garante-se que nenhum registro da tabela Detail, vinculado ao Master, esteja em atualização ou inserção. É fato que dependendo da rotina desenvolvida, é obrigatório iniciar a leitura pela tabela Detail e não pela Master. Nestas situações deve-se garantir que, antes de qualquer atualização do registro Detail, o registro Master seja bloqueado antes do registro Detail.
No exemplo são mencionadas apenas duas tabelas, porém independentemente da quantidade de tabelas envolvidas no formulário, o registro Master deve ser bloqueado.
Por último, existe a prevenção do momento do COMMIT. A arquitetura do Server Application da linha Microsiga Protheus faz com que o momento do COMMIT seja automático, conforme a lógica do programa, assim como pode utilizar mais de um canal de comunicação com o SGBD para leitura. Se o Server Application utiliza mais de um canal para leitura, é possível inferir que os demais canais de leitura somente “visualizarão” os registros, cujos dados foram comitados. Para prevenir qualquer tipo problema na leitura através de querys, é necessário forçar o COMMIT dos dados, conforme demonstrado abaixo.
                                               SPED050->(dbCommit())
BeginSql Alias cAlias
                                                               SELECT R_E_C_N_O_
                                                               FROM SPED050
                                                               WHERE
                                                               ID_ENT = %Exp:cIdEnt% AND
                                                               (STATUS = 1 OR
                                                               STATUS = 2 OR
                                                               STATUSDPEC = 1) AND
                                                               STATUSDPEC <> 5 AND
                                                               %NOTDEL%
                                               EndSql
 
O nível de isolamento READ UNCOMMITTED é o mais aconselhável para aplicação do tipo de aplicação OLTP (On Line Transaction Process),  porém cabe ao desenvolvedor o controle do formato de leitura e o bloqueio do registro para atualização.
Bloqueio de interface
Num ambiente multiusuário a probabilidade de dois usuários alterarem o mesmo registro de um cadastro, simultaneamente, é muito alta. Por esta razão faz-se necessário impedir o acesso de um deles.
O Framework da linha Microsiga Protheus possui a função Softlock para esta finalidade. Esta função deve ser utilizada para bloquear um determinado registro, em atualização, durante as operações de interface, conforme pode ser observado abaixo:
If SoftLock(cAlias)
RegToMemory(cAlias,.F.,lVirtual)
                DEFINE MSDIALOG ___oDlg OF oFather:oWnd FROM 0, 0 TO 0, 0 PIXEL STYLE nOR( WS_VISIBLE, WS_POPUP )
                aPosEnch := {,,,}
                oEnc01:= MsMGet():New( cAlias, nReg, nOpc, ,"CRA", oemtoansi(STR0004), aAcho, aPosEnch ,aCpos,,,,cTudoOk,___oDlg ,,lVirtual,.F.,,,,,,,,cTela) //"Quanto …s altera‡”es?"
                oEnc01:oBox:align := CONTROL_ALIGN_ALLCLIENT
                bEndDlg := {|lOk| If(lOk:=___oDlg:End(),nOpcA:=1,nOpcA:=3), lOk}
                ___oDlg:nWidth := aDim[4]-aDim[2]
                ACTIVATE MSDIALOG ___oDlg ON INIT ( FaMyBar(___oDlg,{|| If(Obrigatorio(aGets,aTela).And.Eval(bOk).And.Eval(bOk2,nOpc),Eval(bEndDlg),(nOpcA:=3,.f.))},{|| nOpcA := 3,___oDlg:End()},aButtons), ___oDlg:Move(aDim[1],aDim[2],aDim[4]-aDim[2], aDim[3]-aDim[1]) )
(cAlias)->(MsGoTo(nReg))
If nOpcA == 1
Begin Transaction
                RecLock(cAlias,.F.)
                For nX := 1 TO FCount()
                               FieldPut(nX,M->&(EVAL(bCampo,nX)))
                Next nX
                If Type("aMemos") == "A"
                               For nX := 1 to Len(aMemos)
                                                cVar := aMemos[nX][2]
                                               cVar1:= aMemos[nX][1]
                                               MSMM(&cVar1,TamSx3(aMemos[nX][2])[1],,&cVar,1, ,,cAlias,aMemos[nX][1],cAliasMemo)
                               Next nX
                EndIf
EndIf
 
A função SoftLock garante que a interface somente seja montada para o primeiro usuário que fez a tentativa de atualização ou exclusão. Enquanto a transação não for abortada ou concluída, o registro permanecerá bloqueado para os demais. Note que apesar da função SoftLock realizar um bloqueio, o uso da função RecLock continua a ser necessário na transação.
Bloqueio de processamento
Em rotinas de processamento em ambientes multiusuários é comum dois usuários solicitarem o mesmo processamento simultaneamente.
Exemplo: Usuário A e B solicitam a liberação de todos os pedidos em aberto. Nesta situação há duas alternativas:
·         Bloquear a rotina para uso Mono-usuário
·         Tratar a concorrência
O bloqueio da rotina para uso Monousuário deve ser evitado, porém se necessário deve ser feito através do uso de um semáforo. A função LockByName é a mais recomendada para esta finalidade. Porém deve-se atentar o isolamento necessário. Caso a rotina seja multiempresa, deve-se considerar a empresa, caso for multifilial deve-se considerar a filial na formação da chave de isolamento. Exemplo: Para que rotina MATA330 esteja isolada de todas as empresas e filiais, deve-se utilizar o Grupo de Empresas na formação da chave de isolamento LockByName(“MATA330_”+cEmpAnt). Se a necessidade for apenas do isolamento da filial, deve-se  utilizar a filial na formação da chave de isolamento LockByName(“MATA330_”+cFilAnt)
Tratar a concorrência em rotinas de processamento é uma tarefa simples, que traz um enorme beneficio para o desempenho da aplicação. Quando dois usuários executam a mesma rotina simultaneamente é muito provável que um dos usuários bloqueie o registro principal e os demais fiquem aguardando a liberação do bloqueio. Após a liberação do bloqueio um dos usuários conseguirá fazer o bloqueio, porém não haverá processamento a ser feito e o próximo registro já estará bloqueado. Como é possível notar, não há beneficio algum para o sistema, mas há o prejuízo de consumo de hardware do Application Server.
 
Para que o sistema tenha beneficio neste tipo de situação é imprescindível o uso da função SimpleLock. Esta função avalia se o registro pode ser bloqueado e caso não consiga, a rotina passará para o próximo registro e assim sucessivamente até o termino do processamento. A grande vantagem de seu uso é que, quanto mais chamadas o cliente fizer da mesma rotina, mais rápido será o processamento da rotina.
Registros da Tabela
Momento
Chamada 1
Chamada 2
Chamada 3
10
1
Bloqueia 10
Bloqueia 11
Bloqueia 12
11
2
Libera 10
Libera 11
Libera 12
12
3
Encerra
Encerra
Encerra
 
Note que este tipo de solução pode ser utilizado em vários tipos de rotina de processamento, desde que haja um ponteiro para execução.
 
Bloqueio de transação
 
As transações tendem a serializar o sistema e deixá-lo mais lento em ambientes multiusuário. Para mitigar a serialização são necessários alguns cuidados.
O primeiro cuidado a se tomar, é evitar um DeadLock. Um sistema está em estado de DeadLock quando existe uma operação (A) fazendo um bloqueio em um registro (R1) e tentando bloquear outro registro (R2). Neste mesmo momento existe outra operação (B) bloqueando o registro (R2) e tentando bloquear o registro (R1). Nesta situação não existe como o banco resolver as solicitações, então ele elege, aleatoriamente, uma das conexões e a encerra provocando um erro para o usuário.
 

Operação A
 
Operação B
R1
Bloqueado
R2
Bloqueado
R2
Aguardando Bloqueio
R1
Aguardando bloqueio

 
Existem duas maneira de resolver um DeadLock. A primeira é garantir que a transação não bloqueie mais de um registro da mesma tabela. Isto pode ser feito reduzindo o tamanho da transação,  de modo a garantir e manter a integridade do sistema.
Exemplo: Ao gravar um formulário do tipo Master/Detail pode-se fazer uma transação para o Master e primeiro registro Detail e outra para os demais itens.
For nX := 1 To Len(aItens)
                BEGIN TRANSACTION
                               If nX == 1
                                               RecLock(“SC5”,.T.)
                                               ...
                               EndIf
                               RecLock(“SC6”,.T.)
                               …
                               …
                               MaAvalSc6()
                CLOSETRANSACTION LOCKIN "SC5"
Next nX
BEGIN TRANSACTION
RecLock(“SC5”)
MaAvalSc5()
END TRANSACTION
 
Note que durante todo o processo, a tabela SC5 está bloqueada e somente é liberada na última transação. Isto é garantido pelo comando CLOSETRANSACTION LOCKIN, que atualiza a transação, mas mantém o bloqueio do registro para não gerar problemas em outros processamentos concorrentes, conforme informado anteriormente.
Outra maneira de resolver,  é utilizando a função MultLock. Esta função garante o bloqueio de todos os registros de uma mesma tabela. A sua desvantagem é o aumento da serialização do produto. Por este motivo, deve-se  utilizá-la com muito cuidado e ter em mente que sempre há uma alternativa para evitar-se o seu uso.
If MultLock("SB2",aMults,1)
                BEGIN TRANSACTION
               
                END TRANSACTION
EndIf
 
A escolha de um dos métodos vai depender do tipo da transação.
Transações de formulário devem, obrigatoriamente, seguir o  primeiro exemplo. As transações são mais rápidas e o risco é a atualização parcial do formulário digitado, porém ele estará íntegro. Com certeza o usuário final prefere perder alguns dados a tudo.
Porém existirão casos em que a atualização parcial não fornece uma transação íntegra. Para estes casos, deve-se  utilizar “MultLock”. Um bom exemplo são as notas fiscais, em que em caso de queda,  o usuário tende a cancelar a nota e refazê-la se houver falta de itens, ou seja, o modelo anterior não traz benefícios ao usuário.

  • Sem rótulos