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.
Visão Geral
Import HTML Content
Conteúdo das Ferramentas
Tarefas