12.2. Isolamento da transação

O padrão SQL define quatro níveis de isolamento de transação em termos de três fenômenos que devem ser evitados entre transações simultâneas. Os fenômenos não desejados são:

dirty read (leitura suja)

A transação lê dados escritos por uma transação simultânea não efetivada (uncommitted). [1]

nonrepeatable read (leitura que não pode ser repetida)

A transação lê novamente dados lidos anteriormente, e descobre que os dados foram alterados por outra transação (que os efetivou após ter sido feita a leitura anterior). [2]

phantom read (leitura fantasma)

A transação executa uma segunda vez uma consulta que retorna um conjunto de linhas que satisfazem uma determinada condição de procura, e descobre que o conjunto de linhas que satisfazem a condição é diferente por causa de uma outra transação efetivada recentemente. [3]

Os quatro níveis de isolamento de transação, e seus comportamentos correspondentes, estão descritos na Tabela 12-1.

Tabela 12-1. Níveis de isolamento da transação no SQL

Nível de isolamento Dirty Read Nonrepeatable Read Phantom Read
Read uncommitted Possível Possível Possível
Read committed Impossível Possível Possível
Repeatable read Impossível Impossível Possível
Serializable Impossível Impossível Impossível

No PostgreSQL pode ser requisitado qualquer um dos quatros níveis de isolamento padrão. Porém, internamente só existem dois níveis de isolamento distintos, correspondendo aos níveis de isolamento Read Committed e Serializable. Quando é selecionado o nível de isolamento Read Committed realmente obtém-se Read Committed, mas quando é selecionado Repeatable Read na realidade é obtido Serializable. Portanto, o nível de isolamento real pode ser mais estrito do que o selecionado. Isto é permitido pelo padrão SQL: os quatro níveis de isolamento somente definem quais fenômenos não podem acontecer, não definem quais fenômenos devem acontecer. O motivo pelo qual o PostgreSQL só disponibiliza dois níveis de isolamento, é porque esta é a única forma de mapear os níveis de isolamento padrão na arquitetura de controle de simultaneidade multiversão que faz sentido. O comportamento dos níveis de isolamento disponíveis estão detalhados nas próximas subseções.

É utilizado o comando SET TRANSACTION para definir o nível de isolamento da transação.

12.2.1. Nível de isolamento Read Committed

O Read Committed (lê efetivado) é o nível de isolamento padrão do PostgreSQL. Quando uma transação processa sob este nível de isolamento, o comando SELECT enxerga apenas os dados efetivados antes da consulta começar; nunca enxerga dados não efetivados, ou as alterações efetivadas pelas transações simultâneas durante a execução da consulta (Entretanto, o SELECT enxerga os efeitos das atualizações anteriores executadas dentro da sua própria transação, mesmo que ainda não tenham sido efetivadas). Na verdade, o comando SELECT enxerga um instantâneo do banco de dados, como este era no instante em que a consulta começou a executar. Deve ser observado que dois comandos SELECT sucessivos podem enxergar dados diferentes, mesmo estando dentro da mesma transação, se outras transações efetivarem alterações durante a execução do primeiro comando SELECT.

Os comandos UPDATE, DELETE e SELECT FOR UPDATE se comportam do mesmo modo que o SELECT para encontrar as linhas de destino: somente encontram linhas de destino efetivadas até o momento do início do comando. Entretanto, no momento em que foi encontrada alguma linha de destino pode ter sido atualizada (ou excluída ou marcada para atualização) por outra transação simultânea. Neste caso, a transação que pretende atualizar fica aguardando a transação de atualização que começou primeiro efetivar ou desfazer (se ainda estiver executando). Se a transação de atualização que começou primeiro desfizer as atualizações, então seus efeitos são negados e a segunda transação de atualização pode prosseguir com a atualização da linha original encontrada. Se a transação de atualização que começou primeiro efetivar as atualizações, a segunda transação de atualização ignora a linha caso tenha sido excluída pela primeira transação de atualização, senão tenta aplicar sua operação na versão atualizada da linha. A condição de procura do comando (a cláusula WHERE) é avaliada novamente para verificar se a versão atualizada da linha ainda corresponde à condição de procura. Se corresponder, a segunda transação de atualização prossegue sua operação começando a partir da versão atualizada da linha.

Devido à regra acima, é possível um comando de atualização enxergar um instantâneo inconsistente: pode enxergar os efeitos dos comandos simultâneos de atualização que afetam as mesmas linhas que está tentando atualizar, mas não enxerga os efeitos destes comandos de atualização nas outras linhas do banco de dados. Este comportamento torna o Read Committed inadequado para os comandos envolvendo condições de procura complexas. Entretanto, é apropriado para casos mais simples. Por exemplo, considere a atualização do saldo bancário pela transação mostrada abaixo:

BEGIN;
UPDATE conta SET saldo = saldo + 100.00 WHERE num_conta = 12345;
UPDATE conta SET saldo = saldo - 100.00 WHERE num_conta = 7534;
COMMIT;

Se duas transações deste tipo tentarem mudar ao mesmo tempo o saldo da conta 12345, é claro que desejamos que a segunda transação comece a partir da versão atualizada da linha da conta. Como cada comando afeta apenas uma linha predeterminada, permitir enxergar a versão atualizada da linha não cria nenhum problema de inconsistência.

Como no modo Read Committed cada novo comando começa com um novo instantâneo incluindo todas as transações efetivadas até este instante, de qualquer modo os próximos comandos na mesma transação vão enxergar os efeitos das transações simultâneas efetivadas. O ponto em questão é se, dentro de um único comando, é enxergada uma visão totalmente consistente do banco de dados.

O isolamento parcial da transação fornecido pelo modo Read Committed é adequado para muitos aplicativos, e este modo é rápido e fácil de ser utilizado. Entretanto, para aplicativos que efetuam consultas e atualizações complexas, pode ser necessário garantir uma visão do banco de dados com consistência mais rigorosa que a fornecida pelo modo Read Committed.

12.2.2. Nível de isolamento serializável

O nível Serializable fornece o isolamento de transação mais rigoroso. Este nível emula a execução serial das transações, como se todas as transações fossem executadas uma após a outra, em série, em vez de simultaneamente. Entretanto, os aplicativos que utilizam este nível de isolamento devem estar preparados para tentar executar novamente as transações, devido a falhas de serialização.

Quando uma transação está no nível serializável, o comando SELECT enxerga apenas os dados efetivados antes da transação começar; nunca enxerga dados não efetivados ou alterações efetivadas durante a execução da transação por transações simultâneas (Entretanto, o comando SELECT enxerga os efeitos das atualizações anteriores executadas dentro da sua própria transação, mesmo que ainda não tenham sido efetivadas). É diferente do Read Committed, porque o comando SELECT enxerga um instantâneo do momento de início da transação, e não do momento de início do comando corrente dentro da transação. Portanto, comandos SELECT sucessivos dentro de uma mesma transação sempre enxergam os mesmos dados.

Os comandos UPDATE, DELETE e SELECT FOR UPDATE se comportam do mesmo modo que o comando SELECT para encontrar as linhas de destino: somente encontram linhas de destino efetivadas até o momento do início da transação. Entretanto, alguma linha de destino pode ter sido atualizada (ou excluída ou marcada para atualização) por outra transação simultânea no momento em que foi encontrada. Neste caso, a transação serializável aguarda a transação de atualização que começou primeiro efetivar ou desfazer as alterações (se ainda estiver executando). Se a transação que começou primeiro desfizer as alterações, então seus efeitos são negados e a transação serializável pode prosseguir com a atualização da linha original encontrada. Porém, se a transação que começou primeiro efetivar (e realmente atualizar ou excluir a linha, e não apenas selecionar para atualização), então a transação serializável é desfeita com a mensagem

ERRO:  não foi possível serializar o acesso devido a atualização simultânea

porque uma transação serializável não pode alterar linhas alteradas por outra transação após a transação serializável ter começado.

Quando o aplicativo receber esta mensagem de erro deverá interromper a transação corrente, e tentar executar novamente toda a transação a partir do início. Da segunda vez em diante, a transação passa a enxergar a alteração efetivada anteriormente como parte da sua visão inicial do banco de dados e, portanto, não existirá conflito lógico em usar a nova versão da linha como ponto de partida para atualização na nova transação.

Deve ser observado que somente as transações que fazem atualizações podem precisar de novas tentativas; as transações somente para leitura nunca estão sujeitas a conflito de serialização.

O modo serializável fornece uma garantia rigorosa que cada transação enxerga apenas visões totalmente consistentes do banco de dados. Entretanto, o aplicativo deve estar preparado para executar novamente a transação quando atualizações simultâneas tornarem impossível sustentar a ilusão de uma execução serial. Como o custo de refazer transações complexas pode ser significativo, este modo é recomendado somente quando as transações efetuando atualizações contêm lógica suficientemente complexa a ponto de produzir respostas erradas no modo Read Committed. Habitualmente, o modo serializável é necessário quando a transação executa vários comandos sucessivos que necessitam enxergar visões idênticas do banco de dados.

12.2.2.1. Isolamento serializável versus verdadeira serialidade

O significado intuitivo (e a definição matemática) de execução "serializável" é que quaisquer duas transações simultâneas efetivadas com sucesso parecem ter sido executadas de forma rigorosamente serial, uma após a outra — embora qual das duas parece ter ocorrido primeiro não pode ser previsto antecipadamente. É importante ter em mente que proibir os comportamentos indesejáveis listados na Tabela 12-1 não é suficiente para garantir a verdadeira serialidade e, de fato, o modo serializável do PostgreSQL não garante a execução serializável neste sentido. Como exemplo será considerada a tabela minha_tabela contendo inicialmente

 classe | valor
--------+-------
      1 |    10
      1 |    20
      2 |   100
      2 |   200

Suponha que a transação serializável A calcula

SELECT SUM(valor) FROM minha_tabela WHERE classe = 1;

e insira o resultado (30) como valor em uma nova linha com classe = 2. Simultaneamente a transação serializável B calcula

SELECT SUM(valor) FROM minha_tabela WHERE classe = 2;

e obtém o resultado 300, que é inserido em uma nova linha com classe = 1. Em seguida as duas transações efetivam. Nenhum dos comportamentos não desejados ocorreu, ainda assim foi obtido um resultado que não poderia ter ocorrido serialmente em qualquer ordem. Se A tivesse executado antes de B, então B teria calculado a soma como 330, e não 300, e de maneira semelhante a outra ordem teria produzido uma soma diferente na transação A.

Para garantir serialidade matemática verdadeira, é necessário que o sistema de banco de dados imponha o bloqueio de predicado, significando que a transação não pode inserir ou alterar uma linha que corresponde à condição WHERE de um comando de outra transação simultânea. Por exemplo, uma vez que a transação A tenha executado o comando SELECT ... WHERE class = 1, o sistema de bloqueio de predicado proibiria a transação B inserir qualquer linha com classe igual a 1 até que A fosse efetivada. [4] Um sistema de bloqueio deste tipo é de implementação complexa e de execução extremamente dispendiosa, uma vez que todas as sessões devem estar cientes dos detalhes de todos os comandos executados por todas as transações simultâneas. E este grande gasto em sua maior parte seria desperdiçado, porque na prática a maioria dos aplicativos não fazem coisas do tipo que podem ocasionar problemas (Certamente o exemplo acima é bastante irreal, dificilmente representando um programa de verdade). Portanto, o PostgreSQL não implementa o bloqueio de predicado, e tanto quanto saibamos nenhum outro SGBD de produção o faz.

Nos casos em que a possibilidade de execução não serial representa um perigo real, os problemas podem ser evitados através da utilização apropriada de bloqueios explícitos. São mostrados mais detalhes nas próximas seções.

Notas

[1]

dirty read — A transação SQL T1 altera uma linha. Em seguida a transação SQL T2 lê esta linha antes de T1 executar o comando COMMIT. Se depois T1 executar o comando ROLLBACK, T2 terá lido uma linha que nunca foi efetivada e que, portanto, pode ser considerada como nunca tendo existido. (ISO-ANSI Working Draft) Foundation (SQL/Foundation), August 2003, ISO/IEC JTC 1/SC 32, 25-jul-2003, ISO/IEC 9075-2:2003 (E) (N. do T.)

[2]

nonrepeatable read — A transação SQL T1 lê uma linha. Em seguida a transação SQL T2 altera ou exclui esta linha e executa o comando COMMIT. Se T1 tentar ler esta linha novamente, pode receber o valor alterado ou descobrir que a linha foi excluída. (ISO-ANSI Working Draft) Foundation (SQL/Foundation), August 2003, ISO/IEC JTC 1/SC 32, 25-jul-2003, ISO/IEC 9075-2:2003 (E) (N. do T.)

[3]

phantom read — A transação SQL T1 lê um conjunto de linhas N que satisfazem a uma condição de procura. Em seguida a transação SQL T2 executa comandos SQL que geram uma ou mais linhas que satisfazem a condição de procura usada pela transação T1. Se depois a transação SQL T1 repetir a leitura inicial com a mesma condição de procura, será obtida uma coleção diferente de linhas. (ISO-ANSI Working Draft) Foundation (SQL/Foundation), August 2003, ISO/IEC JTC 1/SC 32, 25-jul-2003, ISO/IEC 9075-2:2003 (E) (N. do T.)

[4]

Em essência, o sistema de bloqueio de predicado evita leituras fantasmas restringindo o que é escrito, enquanto o MVCC evita restringindo o que é lido.

SourceForge.net Logo CSS válido!