Páginas

terça-feira, 8 de abril de 2014

Transações no PostgreSQL

O que são transações?

Transações são conjuntos de instruções enviadas ao banco de dados que devem ser tratadas com uma única operação. Ou o servidor realiza tudo ou não realiza nada. O exemplo clássico usado em 110% dos cursos de bancos de dados é a famosa transação financeira de transferência de fundos. Imagine que Mônica deseje transferir 100,00 reais para a conta de Cebolinha. Esta operação na realidade se divide em duas. Um débito na conta de Mônica e um crédito de mesmo valor na conta de Cebolinha (a ordem não interfere). Agora, se logo após o débito na primeira conta, o servidor sair do ar antes de que possa executar a segunda. Para onde foram os 100,00 reais?
Existem quatro características que os SGBDs devem garantir se pretendem lidar com transações com segurança. O conjunto dessas características é conhecido como ACID (atomicity, consistency, isolation, durability). Vejamos o que significam:


Atomicidade

É o ponto chave. É quem determina o tudo-ou-nada. A comunidade científica por longos anos acreditou que um átomo seria a unidade mínima da matéria, sólido e indivisível. Já se sabe em simples aulas de química que essa idéia caiu há séculos. No entanto, este conceito serve de analogia para ocasiões onde deve se considerar a impossibilidade de dividir algo em unidades menores.
Sendo assim, a atomicidade é a característica que torna um conjunto de operações um agrupamento indivisível executado por completo ou nada feito.

Consistência

Esta característica determina que o SGBD deve iniciar uma transação em um estado consistente e terminá-la deixando os dados em outro estado consistente. Caso alguma ação dentro de uma transação dê problemas e fira alguma regra de consistência de dados (como uma falha de escrita em disco, por exemplo), toda a transação deve ser completamente abortada, deixando os dados em seu estado consistente imediatamente anterior. O ato de abortar e voltar ao ponto inicial é conhecido como ROLLBACK.
Inconsistências não se resumem a problemas físicos de dados. É possível encarar uma situação fisicamente consistente como logicamente inconsistente. No exemplo dos desaparecimento dos 100,00 reais, o banco ficou em um estado inconsistente uma vez que se somarmos os saldos nas duas contas, independentemente do valor transferido os resultados pré e pós transferência deveria dar consistentemente o mesmo.

Isolamento

Esta característica remete a acessos concorrentes. Caso apenas uma transação seja executada no SGBD em determinado intervalo de tempo, o isolamento não vai lhe acrescentar nada. No entanto, como sabemos que SGBDs, na maioria dos casos, são usados por sistemas concorrentes, esta característica entra para garantir que informações de uma transação não se misturem com de outras.
Esta é a única característica relativa das quatro. Ou seja, é a única que não necessariamente é ruim quando infringida de alguma forma. Por um lado, seria ruim dados de uma transação estarem visíveis a outras como no problema clássico conhecido como Dirty Read que ocorre quando uma transação tx1 modifica alguns registros, uma transação tx2 ao selecionar alguns dados, visualiza esses registros alterados por tx1, mas esta primeira é abortada por alguma razão. Em resumo, tx2 leu dados errados, uma vez que eles nunca deveriam ter sido alterados.
Por outro lado, a capacidade de trabalhar com múltiplas transações ao mesmo tempo pode garantir o bom desempenho em sistemas muito acessados, uma vez que uma não fica esperando pela conclusão da outra. Elas são implementadas em ambientes multi-processos ou multi-threads e podem explorar melhor o pontencial multi-core dos hardwares atuais.

Durabilidade


Toda transação é encerrada com a execução da ação COMMIT. Esta característica é a garantia dada pelo SGBD que quando retornar um sinal COMMIT, os dados já devem estar gravados no meio persistente e ele deve ser capaz de recuperar esses dados em transações posteriores. Caso haja algum problema, a transação deve ser abortada. Conforme a regra da consistência, caso não haja problemas, o banco está jurando de pés juntos que gravou e você vai poder ler depois.
Como tratado no capítulo 1, o PostgreSQL escreve em disco registros de log WAL e assim que estejam completamente escritos, retorna o sinal COMMIT. Já no capítulo 2, tratamos das diretivas do postgresql.conf fsync e wal_sync_method. Para recapitular, a primeira faz com que o SGBD após a execução do comando de escrita do segmento WAL realize consultas ao sistema operacional a fim de obter a garantia de que o valor escrito está completamente descarregado no disco. Já a segunda determina o método adotado nesta pesquisa. Desligar a diretiva fsync pode proporcionar um maior desempenho, mas o risco da falta de durabilidade de uma transação “commitada” em geral não faz valer a pena esse tuning desesperado.
O método de sincronização com o disco de segmentos WAL (wal_sync_method) padrão do Linux é o fdatasync. O método fsync aguarda o sistema operacional descarregar em disco toda a pilha do buffer de I/O e atualizar o inode do sistema de arquivos com a informação de última alteração do arquivo escrito. O fdatasync não espera pela atualização deste metadado. Ele se preocupa apenas com o que mais importa que é a atualização propriamente dita dos dados.

Transações e Write-ahead Logging


Ao confirmar cada transação, o servidor não registra em disco as páginas de dados (aqueles 8Kb que ficam na memória). É bem menos custoso registrar neste momento (e esperar pelo fsync), um registro de segmento WAL uma vez que ele é linear (alterações na base real podem envolver dados que não necessariamente estejam na mesma página de dados) e por isso realizam movimentos menores da controladora de disco, além de não requerer pesquisa por espaço livre (FSM).
Por outro lado, a manipulação das páginas de memória compartilhada que são carregadas do e para o disco é bem mais vantajoso do que sair vasculhando registros de log de operações a fim de obter os dados necessários nas consultas.
Sendo assim, o PostgreSQL precisa realizar essa tradução do que é salvo em log para as páginas de dados de tempos em tempos. Este processo de escrita em background (bgwriter) é denominado CHECKPOINT. No PostgreSQL, checkpoints não têm ligação direta com o COMMIT de uma transação, mas achei interessante ilustrar o que deve ocorrer com algumas transações confirmadas entre um checkpoint e um provável crash da base.

Problemas de isolamento

Transações já são interessantes mesmo em cenário standalone, uma vez que é possível desfazer todo um bloco de procedimentos apenas com um simples Rollback. Mas é em sistemas concorrentes que o bicho pega. Caso o isolamento não seja garantido alguns problemas podem ocorrer.

Dirty read

O primeiro deles já foi apontado. Dirty Read ou leitura suja acontece quando uma transação lê algum dado alterado por outra que acaba não confirmando suas alterações. É um problema extremo que de fato não é comum que bancos de dados permitam que ocorra. O PostgreSQL não permite. Confira no exercício abaixo.

Primeiramente, vamos criar a base para os testes:
01 postgres=# CREATE DATABASE exercicio;
02 CREATE DATABASE
03 postgres=# \c exercicio
04 You are now connected to database "exercicio".
05 exercicio=# create table aluno (id serial primary key, nome varchar(50));
06 NOTICE:  CREATE TABLE will create implicit sequence "aluno_id_seq" for serial column "aluno.id"
07 NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "aluno_pkey" for table "aluno"
08 CREATE TABLE
09 exercicio=# INSERT into aluno (nome) values ('Huguinho'), ('Zezinho'), ('Luisinho'), ('Juquinha'), ('Joaozinho');
10 INSERT 0 5

Criamos 5 alunos (só peça boa =) em uma nova base. Agora vamos ao primeiro cenário. Para isto será necessário que você abra duas instâncias do psql ou qualquer outra ferramenta cliente (que certamente não será tão limpa e cheirosa quanto o psql).

Inicie uma transação em cada uma executando em ambas a instrução:
1 exercicio=# begin;
2 BEGIN

Na primeira, execute o seguinte comando:
01 exercicio=# UPDATE aluno set nome = 'Jose da Silva' where id = 2;
02 UPDATE 1
03
04 exercicio=# select * from aluno;
05 id |     nome
06 ----+---------------
07 1 | Huguinho
08 3 | Luisinho
09 4 | Juquinha
10 5 | Joaozinho
11 2 | Jose da Silva
12 (5 rows)

Veja que o nome de Zezinho agora é Jose da Silva.
Realize a mesma consulta na segunda instância do psql:
1 exercicio=# select * from aluno;
2 id |   nome
3 ----+-----------
4 1 | Huguinho
5 2 | Zezinho
6 3 | Luisinho
7 4 | Juquinha
8 5 | Joaozinho
9 (5 rows)

Como você atentou, os alunos continuam com seus nomes intactos. O que a primeira transação modificou não foi sentido pela segunda. O motivo é que se a primeira voltar atrás, a segunda já teria lido e tomado suas próprias decisões baseando-se em dados inconsistentes. Confira como faríamos para reverter (execute na primeira transação):
01 exercicio=# ROLLBACK ;
02 ROLLBACK
03 exercicio=# select * from aluno;
04 id |   nome
05 ----+-----------
06 1 | Huguinho
07 2 | Zezinho
08 3 | Luisinho
09 4 | Juquinha
10 5 | Joaozinho
11 (5 rows)

Nossa base voltou ao estágio em que estava no momento do BEGIN.

Unrepeatable Reads

Leituras não repetíveis acontecem quando uma transação lê algum dado em determinado momento, posteriormente outra transação altera ou deleta e executa o COMMIT. A primeira volta a ler os mesmos dados e constata que eles não estão mais como antes. Este não chega a ser um problema em muitos sistemas, a ponto do PostgreSQL, em seu nível de isolamento padrão, permitir que ocorra. Vejamos:
PSQL1: Iniciamos uma nova transação (aquela anterior foi desfeita com ROLLBACK) e atualizamos novamente o nome de Zezinho.
1 exercicio=# BEGIN ;
2 BEGIN
3 exercicio=# UPDATE aluno set nome = 'Jose da Silva' where id = 2;
4 UPDATE 1

PSQL2: Como visto antes, nada foi percebido pela outra transação.
1 exercicio=# select * from aluno;
2 id |   nome
3 ---+-----------
4 1 | Huguinho
5 2 | Zezinho
6 3 | Luisinho
7 4 | Juquinha
8 5 | Joaozinho
9 (5 rows)

PSQL1: Confirmamos a alteração
1 exercicio=# COMMIT ;
2 COMMIT

PSQL2: Agora a alteração foi sentida
1 exercicio=# select * from aluno;
2 id |     nome
3 ---+---------------
4 1 | Huguinho
5 3 | Luisinho
6 4 | Juquinha
7 5 | Joaozinho
8 2 | Jose da Silva
9 (5 rows)

Phantom Read

O problema das leituras fantasmas é parecido com o das leituras não repetíveis, o que muda é que uma das transações em vez de alterar o conjunto de dados, insere um dado novo.
Vamos refazer a dinâmica. Primeiramente, encerre as transações das duas instâncias do psql. Neste momento, tanto faz encerrar com COMMIT ou com ROLLBACK. Se a transação não estiver em curso, você receberá uma mensagem como esta:
WARNING:  there is no transaction in progress
PSQL1: Inicie a transação e insira duas novas alunas
1 # begin;
2 BEGIN
3 exercicio=# INSERT INTO aluno (nome) values ('Chiquinha'), ('Mariazinha');
4 INSERT 0 2

PSQL2: Inicie a transação e consulte a lista de alunos
01 # begin;
02 BEGIN
03 exercicio=# select * from aluno;
04 id |     nome
05 ----+---------------
06 1 | Huguinho
07 3 | Luisinho
08 4 | Juquinha
09 5 | Joaozinho
10 2 | Jose da Silva
11 (5 rows)

Ninguém novo até agora.
PSQL1: Confirme sua transação com um COMMIT
1 exercicio=# COMMIT ;
2 COMMIT

PSQL2: Veja que as novas alunas agora estão presentes
01 exercicio=# select * from aluno;
02 id |     nome
03 ----+---------------
04 1 | Huguinho
05 3 | Luisinho
06 4 | Juquinha
07 5 | Joaozinho
08 2 | Jose da Silva
09 6 | Chiquinha
10 7 | Mariazinha
11 (7 rows)

Níveis de isolamento

Existem quatro níveis de isolamento segundo o padrão SQL:

READ UNCOMMITED

Nível menos isolado. Aqui, todos os problemas citados podem ocorrer, inclusive as porcas dirty reads. É muito difícil um SGBD permitir que o DBA mesmo que deliberadamente configure o nível de isolamento para READ UNCOMMITED. Em resumo, é um nível acadêmico, apenas para você dizer que conhece.

READ COMMITED

Nível padrão do PostgreSQL. Aqui dirty reads estão barradas, mas, como verificamos nos exercícios anteriores, unrepeatable reads e phantom reads podem ocorrer.

REPEATABLE READ

Neste nível, apenas as phantom reads podem ocorrer. Os SGBDs fazem bloqueio (lock) do conjunto de dados lidos em uma transação, o que evita que as leituras posteriores apresentem resultados modificados ou deletados confirmados por outra transação. No entanto, não há como fazer lock de um registro que ainda não existia na primeira leitura. Este é o motivo pelo qual as leituras fantasmas podem assombrar por aqui.

SERIALIZABLE

Esse é o nível que leva o isolamento ao máximo. Aqui, nenhum desses problemas podem ocorrer.

Em resumo:

A tabela abaixo mostra os níveis de isolamento em relação aos problemas que podem ou não provocar.
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

Para modificar o nível de isolamento da transação corrente, existe o comando SET TRANSACTION que deve ser executado logo após o BEGIN e antes de qualquer consulta SELECT, INSERT, DELETE, UPDATE, FETCH ou COPY. Este comando possui o seguinte formato:
SET TRANSACTION [ISOLATION LEVEL { SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED } ]  [ {READ WRITE | READ ONLY} ]
Com ele, é possível determinar o nível de isolamento, escrevendo ISOLATION LEVEL seguido por algum dos níveis já apresentados e/ou informar as opções READ WRITE (padrão) ou READ ONLY. Transações READ ONLY (somente leitura) não podem alterar nenhuma tabela que não seja temporária.
Vamos a um exemplo:
01 exercicio=# begin;
02 BEGIN
03 exercicio=# set transaction read only;
04 SET
05 exercicio=# UPDATE aluno SET nome = 'teste';
06 ERROR:  cannot execute UPDATE in a read-only transaction
07 exercicio=# select * from aluno;
08 ERROR:  current transaction is aborted, commands ignored until end of transaction block
09 exercicio=# rollback;
10 ROLLBACK

Após o BEGIN, determinamos que nossa transação seria somente leitura. Logo após, tentamos infringir o determinado em SET TRANSACTION executando um UPDATE qualquer. Confira o erro resultante. Segui adiante para mostrar algo que ainda não havia falado, mas ocorre sempre que uma instrução retorna erro dentro de uma transação. Qualquer nova instrução retorna o erro informando que a transação foi abortada. Cabe-nos apenas executar o ROLLBACK para encerrar a transação problemática.
O nível de isolamento REPEATABLE READ no PostgreSQL é encarado como um SERIALIZABLE, ou seja, não permite leituras fantasmas. Vejamos:
PSQL1: Determinando o nível de isolamento para REPEATABLE READ, que no PostgreSQL funciona como um SERIALIZABLE
01 exercicio=# BEGIN ;
02 BEGIN
03 exercicio=# SET transaction ISOLATION LEVEL REPEATABLE READ ;
04 SET
05 exercicio=# select * from aluno;
06  id |     nome
07 ----+---------------
08  1 | Huguinho
09  3 | Luisinho
10  4 | Juquinha
11  5 | Joaozinho
12  2 | Jose da Silva
13  6 | Chiquinha
14  7 | Mariazinha
15 (7 rows)

PSQL2: Inserindo um novo aluno e confirmando a transação
1 exercicio=# BEGIN ;
2 BEGIN
3 exercicio=# INSERT INTO aluno (nome) values ('Maradona');
4 INSERT 0 1
5 exercicio=# COMMIT ;

PSQL1: Olha aí que Maradona ainda não foi inserido e não é porque ele é argentino. Após a confirmação deste transação, ele vai aparecer. Só espero que não estrague a festa.
01 exercicio=# select * from aluno;
02  id |     nome
03 ----+---------------
04  1 | Huguinho
05  3 | Luisinho
06  4 | Juquinha
07  5 | Joaozinho
08  2 | Jose da Silva
09  6 | Chiquinha
10  7 | Mariazinha
11 (7 rows)
12
13 exercicio=# COMMIT ;
14 COMMIT
15 exercicio=# select * from aluno;
16  id |     nome
17 ----+---------------
18  1 | Huguinho
19  3 | Luisinho
20  4 | Juquinha
21  5 | Joaozinho
22  2 | Jose da Silva
23  6 | Chiquinha
24  7 | Mariazinha
25  8 | Maradona
26 (8 rows)

O nível de isolamento READ UNCOMMITED é encarado como um READ COMMITED (o padão). Assim, leituras sujas jamais são permitidas, no PostgreSQL.

Controle de versão

Ao contrário de alguns bancos de dados, o PostgreSQL por padrão trabalha com controle de versão ao invés de bloqueio de dados na manipulação de transações. Este mecanismo, conhecido como MVCC (multi-version concurrency control) costuma ser bem mais eficiente em termos de desempenho em comparação aos bloqueios de registros que já são bem melhores do que os bloqueios de tabelas. Além do PostgreSQL, bancos como Oracle, Firebird, HiperSonic, SyBase, ZODB, entre outros também se baseiam no MVCC para controle de isolamento de transações.
Para cada registro de cada tabela sempre existem dois campos utilizados no controle de transações. O campo xmin informa o id (auto-sequenciado) da transação que gerou este registro. Já xmax determina o id da transação que a deletou ou alterou seu conteúdo, gerando um novo registro com xmin atualizado. Para ilustrar como funciona o controle, vejamos o exemplo:
PSQL1:
01 exercicio=# BEGIN ;
02 BEGIN
03 exercicio=# select *, xmin, xmax from aluno;
04 id |     nome      | xmin | xmax
05 ---+---------------+------+------
06  1 | Huguinho      |  664 |    0
07  3 | Luisinho      |  664 |    0
08  4 | Juquinha      |  664 |    0
09  5 | Joaozinho     |  664 |    0
10  2 | Jose da Silva |  668 |    0
11  6 | Chiquinha     |  669 |    0
12  7 | Mariazinha    |  669 |    0
13  8 | Maradona      |  670 |    0
14 (8 rows)
15
16 exercicio=# DELETE FROM aluno WHERE nome = 'Maradona';
17 DELETE 1
18 exercicio=# select *, xmin, xmax from aluno;
19  id |     nome      | xmin | xmax
20 ----+---------------+------+------
21  1 | Huguinho      |  664 |    0
22  3 | Luisinho      |  664 |    0
23  4 | Juquinha      |  664 |    0
24  5 | Joaozinho     |  664 |    0
25  2 | Jose da Silva |  668 |    0
26  6 | Chiquinha     |  669 |    0
27  7 | Mariazinha    |  669 |    0
28 (7 rows)
29 exercicio=# UPDATE aluno SET nome = 'Zezinho' where id = 2;
30 UPDATE 1
31 exercicio=# select *, xmin, xmax from aluno;
32  id |    nome    | xmin | xmax
33 ----+------------+------+------
34  1 | Huguinho   |  664 |    0
35  3 | Luisinho   |  664 |    0
36  4 | Juquinha   |  664 |    0
37  5 | Joaozinho  |  664 |    0
38  6 | Chiquinha  |  669 |    0
39  7 | Mariazinha |  669 |    0
40  2 | Zezinho    |  671 |    0
41 (7 rows)

No primeiro SELECT, podemos visualizar o xmin de cada registro. Perceba que Jose da Silva possui um xmin maior do que os outros alunos que foram inseridos no mesmo momento. Isto se deve aos UPDATEs que executamos nos exemplos posteriores ao primeiro INSERT.
Realizamos um DELETE para excluir Maradona de onde ele não foi chamado. O SELECT posterior não tem como vê-lo uma vez que o xmax é justamente o id da transação atual. Após um UPDATE para voltarmos com o nome Zezinho do aluno 2, e eis que surgiu um registro no fim da fila com id de transação maior do que todos. O registro com Jose da Silva também não está mais visível aqui.
Agora vamos a uma outra transação antes de confirmação as alterações na atual.
PSQL2: Selecionando os registros em outra transação antes do COMMIT da anterior
01 exercicio=# SELECT *, xmin, xmax FROM aluno;
02  id |     nome      | xmin | xmax
03 ----+---------------+------+------
04  1 | Huguinho      |  664 |    0
05  3 | Luisinho      |  664 |    0
06  4 | Juquinha      |  664 |    0
07  5 | Joaozinho     |  664 |    0
08  2 | Jose da Silva |  668 |  671
09  6 | Chiquinha     |  669 |    0
10  7 | Mariazinha    |  669 |    0
11  8 | Maradona      |  670 |  671
12 (8 rows)

Nela, é possível visualizar os xmax de Jose da Silva e Maradona com o valor da transação em PSQL1. Como a primeira ainda não confirmou a transação, ainda não estamos vendo os novos valores aqui (só veríamos se o nível de isolamento fosse READ UNCOMMITED, que permite dirty reads e não é suportado pelo PostgreSQL).
Vamos confirmar e visualizar o resultado final:
PSQL1: Confirmação e visualização do resultado
01 exercicio=# COMMIT ;
02 COMMIT
03 exercicio=# select *, xmin, xmax from aluno;
04  id |    nome    | xmin | xmax
05 ----+------------+------+------
06  1 | Huguinho   |  664 |    0
07  3 | Luisinho   |  664 |    0
08  4 | Juquinha   |  664 |    0
09  5 | Joaozinho  |  664 |    0
10  6 | Chiquinha  |  669 |    0
11  7 | Mariazinha |  669 |    0
12  2 | Zezinho    |  671 |    0
13 (7 rows)

PSQL2: Visualização atualizada. Se estivesse no nível de isolamento SERIALIZABLE ou REPEATABLE READ, não seria possível ver os registros atualizados e deletados.
01 exercicio=# SELECT *, xmin, xmax FROM aluno;
02 id |    nome    | xmin | xmax
03 ----+------------+------+------
04 1 | Huguinho   |  664 |    0
05 3 | Luisinho   |  664 |    0
06 4 | Juquinha   |  664 |    0
07 5 | Joaozinho  |  664 |    0
08 6 | Chiquinha  |  669 |    0
09 7 | Mariazinha |  669 |    0
10 2 | Zezinho    |  671 |    0
11 (7 rows)

MVCC e a necessidade de Vacuum

O MVCC traz uma série de benefícios sobre os bloqueios:
  • Processos de leitura não bloqueiam processos de escrita e vice-versa
  • Praticamente elimina a incidência de contenções de transações
  • Reduz drasticamente o risco de DEAD LOCK
  • Possui um desempenho bem superior
Mas também é possível detectar um ponto fraco no MVCC. Os registros alterados e excluídos não ficam mais acessíveis mas não são imediatamente riscados do mapa. Ou seja, continuam inchando o disco. Para evitar que o banco tome proporções além do aceitável (este aceitável é bem relativo), execute regularmente (este regularmente é mais relativo ainda) a instrução VACUUM, que além de apagar os registros (VACUUM FULL) ou simplesmente apontando nas páginas de dados como área disponível para ser sobrescrita (VACUUM simples), também atualiza o ID das transações para evitar estouro e consequente reinício automático dos IDs de transações, o que poderia proporcionar resultados catrastróficos (como uma espécie de bug do milênio).
Como os IDs das transações são inteiros de 32 bits, a cada 4 bilhões lá vai fumaça de transações, isto poderá ocorrer. Voltaremos a este assunto em uma seção específica para o comando VACUUM, nos policiando para não realizar mais de 4 bilhões de exemplos daqui até lá.

Bloqueios e deadlocks

Provavelmente, você não irá precisar trabalhar com bloqueios explícitos no PostgreSQL, mas existem situações onde podem ser úteis. A prova é que o PostgreSQL implicitamente trabalha com bloqueios em algumas instruções. Pra variar, vamos a um exemplo:
PSQL1: Atualizando o nome de Mariazinha
1 exercicio=# BEGIN ;
2 BEGIN
3 exercicio=# UPDATE aluno SET nome = 'Maria Francisca' where id = 7;
4 UPDATE 1

PSQL2: Atualizando os nomes de Chiquinha e Mariazinha em outra transação
1 exercicio=# BEGIN ;
2 BEGIN
3 exercicio=# UPDATE aluno SET nome = 'Francisca' where id = 6;
4 UPDATE 1
5
6 exercicio=# UPDATE aluno SET nome = 'Maria Joaquina' where id = 7;

Até o primeiro UPDATE, fora o dilúvio que se abateu sobre Recife e o temporal do lado de fora da janela, estava tudo normal. No segundo, a chuva até que diminuiu, mas a alteração do nome de Mariazinha ficou congelada (bloqueada) até que o PSQL1 confirme a sua alteração. Caso não ficasse, a transação que encerrasse primeiro teria suas alterações perdidas devido a outra que encerrasse depois, independentemente de quem realizou o UPDATE primeiro. Isto resultaria em um outro problema de isolamento conhecido por Lost Update, que pode ainda ser mais grave se uma transação precisar dos dados da outra para atualizar os seus, como eles não estarão disponíveis até o COMMIT, o estado final potencialmente deverá ferir o princípio da consistência.
Agora vamos voltar à primeira transação não para confirmar a alteração e liberar o UPDATE na segunda, mas para pôr mais lenha na fogueira e também esperar pela segunda devido a alteração do nome de Chiquinha.
PSQL1: Atualizando um registro já modificado pela outra transação
1 exercicio=# UPDATE aluno SET nome = 'Chica' where id = 6;
2
3 ERROR:  deadlock detected
4 DETAIL:  Process 9019 waits for ShareLock on transaction 674; blocked by process 8919.
5 Process 8919 waits for ShareLock on transaction 676; blocked by process 9019.
6 Process 9019: UPDATE aluno SET nome = 'Maria Joaquina' where id = 7;
7 Process 8919: UPDATE aluno SET nome = 'Chica' where id = 6;
8 HINT:  See server log for query details.

Booom, DEAD LOCK. Game over. Façamos um breve parênteses para a mensagem de erro do PostgreSQL que não é uma simples mensagem, é uma aula sobre o assunto. Como sei que você é uma pessoa bastante atenta, posso apostar que percebeu o termo ShareLock da mensagem de erro acima. Ela significa bloqueio compartilhado.
Antes de seguir adiante, façamos mais uma dinâmica a fim de colher informações do PostgreSQL acerca dos bloqueios realizados implícita ou explicitamente em determinado momento. A view de catálogo pg_locks recupera os locks gerenciados no momento pelo pelo servidor. Unindo ela com as tabelas de catálogo pg_class e pg_database podemos ter uma informação bem interessante:
PSQL1: Iniciando uma inofensiva transação para mais uma vez alterar o nome de Chiquinha e consultando o bloqueio gerado
01 exercicio=# BEGIN ;
02 BEGIN
03 exercicio=# UPDATE aluno SET nome = 'Chica' where id = 6;
04 UPDATE 1
05
06 exercicio=# select l.locktype, l.mode, l.pid, d.datname, c.relname from pg_locks l inner join pg_database d on l.database = d.oid inner join pg_class c on l.relation = c.oid where c.relkind = 'r' and relname not like 'pg_%';
07 locktype |       mode       | pid  |  datname  | relname
08 ----------+------------------+------+-----------+---------
09 relation | RowExclusiveLock | 8919 | exercicio | aluno
10 (1 row)

O PostgreSQL reconhece oito tipos de bloqueios: ACCESS SHARE, ROW SHARE, ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE.
É possível solicitar explicitamente um bloqueio sobre tabelas ou mesmo registros através do comando Lock:
01 exercicio=# \h LOCK
02 Command:     LOCK
03 Description: lock a table
04 Syntax:
05 LOCK [ TABLE ] [ ONLY ] name [, ...] [ IN lockmode MODE ] [ NOWAIT ]
06
07 where lockmode is one of:
08
09 ACCESS SHARE | ROW SHARE | ROW EXCLUSIVE | SHARE UPDATE EXCLUSIVE
10 | SHARE | SHARE ROW EXCLUSIVE | EXCLUSIVE | ACCESS EXCLUSIVE

Access Share

Bloqueio requerido apenas por instruções SELECT e ANALYZE são simples leitura em geral permitidas, a menos que alguma outra transação tenha requerido Access Exclusive.

Row Share

A instrução SELECT com a cláusula FOR UPDATE obtém este nível de bloqueio. Ele conflita apenas com os níveis mais retritos Exclusive e Access Exclusive.

Row Exclusive

Bloqueio solicitado automaticamente por instruções INSERT, UPDATE, DELETE. Conflita com quem precisar além de Access Exclusive e Exclusive, também com Share Row Exclusive e Share.

Share Update Exclusive

Bloqueio padrão do comando VACUUM sem a opção FULL. Conflita com todos os próximos níveis de bloqueio e com ele próprio. Bloqueia a tabela inteira.

Share

A partir deste nível de bloqueio, já vimos com quem cada um conflita. Bloqueia toda a tabela e é requerido por instruções CREATE INDEX.

Share Row Exclusive

Similar ao Exclusive, mas apenas no nível de linha. Não há instrução SQL que automaticamente solicite este nível de bloqueio.

Exclusive

Este só não conflita com Access Share. É requerido automaticamente apenas em tabelas de catálogo em determinadas operações.

Access Exclusive

Conflita com todo mundo. Ou seja, nenhuma outra transação pode operar na mesma tabela bloqueada por alguma transação que solicitou Access Exclusive. Instruções ALTER TABLE, DROP TABLE, TRUNCATE, REINDEX, CLUSTER e VACUUM FULL necessitam deste modo de bloqueio.

Conclusão

Percebeu como é denso e fundamental o assunto transações? Elas são provavelmente o mais frequente motivo para desenvolvedores optarem por trabalhar com SGBDs em vez de persistirem dados diretamente em arquivos.
Acredito que tanto DBAs quanto desenvolvedores precisam conhecer bem como o SGBD escolhido lida com elas e optar pelo PostgreSQL é optar pelo que há de mais avançado no mercado sobre o assunto.

Origem: http://especializa.com.br/blog/2010/06/27/transacoes-no-postgresql/

Nenhum comentário:

Postar um comentário