postgres postgresql mvcc transaction acid atomicity consistency isolation durability

MVCC

O Multi Version Concurrency Control (MVCC, controle de concorrência por múltiplas versões ou por multiversionamento) é uma das muitas possíveis implementações que atendem os requisitos de isolamento de transações e visibilidade de tuplas que sua aplicação exige. Ainda assim, é a implementação escolhida pelo PostgreSQL e por diversos outros sistemas.

Esse método consiste em guardar versões diferentes de cada tupla (termo acadêmico para a linha da tabela) associadas a tempos de vida útil delas, também chamados de visibilidade da tupla. Uma tupla que tenha sido inserida por uma transação só é visível daquele momento em diante; uma tupla que tenha sido removida não é mais visível a partir daquele momento; e uma que tenha sofrido uma alteração existe em duas versões, sendo que apenas uma versão é visível para transações em cada momento.

Partindo de um exemplo, considere uma tabela com três colunas e duas linhas. Quando aplicação consulta a tabela com um SELECT * FROM tabela;, ela recebe esse resultado:

┌─id─┬─Nome─┬─Idade─┐
│  1 │ Alice│    23 │
├────┼──────┼───────┤
│  2 │ Bob  │    34 │
└────┴──────┴───────┘

Internamente o PostgreSQL guarda dois campos extras que demarcam a visibilidade de cada linha: xmin e xmax. A visibilidade é representada em função do identificador de transação (xid em alguns contextos, pois o x costuma representar "transação"), que é um número monotonicamente crescente. Esse número tem valores especiais indicando o passado distante e o futuro distante, mas aqui vamos usar ∞. Podemos ver os campos extras mencionando eles explicitamente no SELECT xmin, xmax, * FROM tabela;, assim:

╔═xmin═╤═xmax═╤═id═╤═Nome════╤═Idade═╗
║  -∞  │   ∞  │  1 │ Alice   │    23 ║
╟──────┼──────┼────┼─────────┼───────╢
║  -∞  │   ∞  │  2 │ Bob     │    34 ║
╚══════╧══════╧════╧═════════╧═══════╝

Nesse exemplo, as duas linhas estão com visibilidade completa, então todas as transações as veem; e isso inclui transações que iniciaram no passado distante e as que ainda não foram iniciadas.

Mas quando uma transação T1 faz uma nova inserção na tabela:

[[local]:5432] postgres@postgres=# BEGIN;  -- iniciando T1
[[local]:5432] postgres@postgres=# INSERT INTO tabela (id, nome, idade) VALUES (3, 'Charlie', 45);
[[local]:5432] postgres@postgres=# COMMIT; -- finalizando T1

O resultado que temos é o seguinte:

╔═xmin═╤═xmax═╤═id═╤═Nome════╤═Idade═╗
║  -∞  │   ∞  │  1 │ Alice   │    23 ║
╟──────┼──────┼────┼─────────┼───────╢
║  -∞  │   ∞  │  2 │ Bob     │    34 ║
╟──────┼──────┼────┼─────────┼───────╢
║  T1  │   ∞  │  3 │ Charlie │    45 ║
╚══════╧══════╧════╧═════════╧═══════╝

Neste momento, quando a aplicação consulta a tabela ela pode receber duas ou três linhas de resposta, dependendo do seu nível de isolamento e de quando foi iniciada. Transações mais antigas que T1 em níveis SERIALIZABLE ou REPEATABLE READ ainda receberão apenas duas linhas; mas transações mais antigas que T1 em níveis READ COMMITTED ou READ UNCOMMITTED, assim como transações mais recentes que T1 receberão três linhas, como esperado para cada nível de isolamento.

Ao apagar uma linha, o PostgreSQL define a data de fim de validade, o que faz com que outras transações deixem de ver a tupla, por exemplo com a transação T2:

[[local]:5432] postgres@postgres=# BEGIN;  -- iniciando T2
[[local]:5432] postgres@postgres=# DELETE FROM tabela where id = 2;
[[local]:5432] postgres@postgres=# COMMIT; -- finalizando T2

Transações mais antigas ainda verão as três linhas, mas agora com a visibilidade final reduzida:

╔═xmin═╤═xmax═╤═id═╤═Nome════╤═Idade═╗
║  -∞  │   ∞  │  1 │ Alice   │    23 ║
╟──────┼──────┼────┼─────────┼───────╢
║  -∞  │  T2  │  2 │ Bob     │    34 ║
╟──────┼──────┼────┼─────────┼───────╢
║  T1  │   ∞  │  3 │ Charlie │    45 ║
╚══════╧══════╧════╧═════════╧═══════╝

Ou seja, transações mais antigas ainda receberão as linhas 1 e 2, mas não a linha 3. Transações mais recentes receberão as linhas 1 e 3.

Alterações duplicam a tupla, já que as transações em andamento podem precisar ver toda a tupla inteiramente como estava no passado, mas transações novas precisam ver a versão atual. Então com uma transação T3:

[[local]:5432] postgres@postgres=# BEGIN;  -- iniciando T3
[[local]:5432] postgres@postgres=# UPDATE tabela SET idade = 56 WHERE id = 1;
[[local]:5432] postgres@postgres=# COMMIT; -- finalizando T3

E agora a tabela tem quatro versões de três tuplas:

╔═xmin═╤═xmax═╤═id═╤═Nome════╤═Idade═╗
║  -∞  │  T3  │  1 │ Alice   │    23 ║
╟──────┼──────┼────┼─────────┼───────╢
║  T3  │   ∞  │  1 │ Alice   │    56 ║
╟──────┼──────┼────┼─────────┼───────╢
║  -∞  │  T2  │  2 │ Bob     │    34 ║
╟──────┼──────┼────┼─────────┼───────╢
║  T1  │   ∞  │  3 │ Charlie │    45 ║
╚══════╧══════╧════╧═════════╧═══════╝

Cada comando, como um SELECT, tem um identificador de transação, ou xid. Mesmo comandos executados sem BEGINs explícitos recebem um xid (nesse caso a transação é chamada de virtual). Quando o SELECT caminha pela tabela, seu xid é comparado com xmin e xmax de cada versão existente e apenas as tuplas com visibilidade naquela transação (xmin <= xid < xmax) são retornadas à aplicação. Assim, apenas uma versão da tupla da Alice é apresentada para cada transação, em função do seu nível de isolamento e do seu xid.

Isso significa que neste momento, mesmo o SELECT que temos usado para investigar os xmin e xmax não mostram todas as versões de tuplas da Alice, que são mutuamente excludentes.

Mas é possível usar um módulo de depuração para investigar o conteúdo da tabela, contornando essa filtragem de visibilidade:

[[local]:5432] postgres@postgres=# SELECT * FROM heap_page_items(get_raw_page('tabela', 0));

A consulta retorna todas as versões de tuplas contidas na página 0 (8kB) para a tabela. Diversas outras informações são trazidas, assim como números específicos de xid e representações internas dos dados armazenados. Mas conceitualmente, o que recebemos é:

┏━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━┓
┃     │  [-∞|T3|1|Alice|23]   ┃
┃Bloco│  [T3|∞|1|Alice|56]    ┃
┃ 8kB │  [-∞|T2|2|Bob|34]     ┃
┃     │  [T1|∞|3|Charlie|45]  ┃
┗━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━┛

Prática

Vamos investigar o comportamento do MVCC.

  1. Inicie uma conexão (C1), crie uma tabela e deixe ela em uma transação em modo SERIALIZABLE:
    [[local]:5432] postgres@postgres=# CREATE TABLE tabela (id int, nome text, idade int);
    [[local]:5432] postgres@postgres=# BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    [[local]:5432] postgres@postgres*=#
    
  2. Em uma outra conexão (C2), observe o xid e insira algumas linhas dentro e fora de transações explícitas:
    [[local]:5432] postgres@postgres=# BEGIN;
    [[local]:5432] postgres@postgres*=# SELECT txid_current();
    [[local]:5432] postgres@postgres*=# INSERT INTO tabela VALUES (1, 'Alice', 23);
    [[local]:5432] postgres@postgres*=# INSERT INTO tabela VALUES (2, 'Bob', 34);
    [[local]:5432] postgres@postgres*=# COMMIT;
    [[local]:5432] postgres@postgres=# INSERT INTO tabela VALUES (3, 'Charlie', 45);
    [[local]:5432] postgres@postgres=# SELECT xmin, xmax, * FROM tabela;
    
  3. Consulte a tabela na conexão C1:
    [[local]:5432] postgres@postgres*=# SELECT xmin, xmax, * from tabela;
    
  4. Inicie uma terceira conexão (C3) em modo READ COMMITED:
    [[local]:5432] postgres@postgres=# BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
    [[local]:5432] postgres@postgres*=#
    
  5. Na conexão C2, observe o xid e altere as linhas, sem executar o COMMIT:
    [[local]:5432] postgres@postgres=# BEGIN;
    [[local]:5432] postgres@postgres*=# SELECT txid_current();
    [[local]:5432] postgres@postgres*=# DELETE FROM tabela WHERE id = 2;
    [[local]:5432] postgres@postgres*=# UPDATE tabela SET idade = 56 WHERE id = 1;
    
  6. Consulte a tabela em todas as conexões C1, C2 e C3:
    [[local]:5432] postgres@postgres=# SELECT xmin, xmax, * from tabela;
    
  7. Execute o COMMIT na conexão C2:
    [[local]:5432] postgres@postgres*=# COMMIT;
    
  8. Consulte a tabela em todas as conexões C1, C2 e C3:
    [[local]:5432] postgres@postgres=# SELECT xmin, xmax, * from tabela;
    
  9. Em qualquer uma das conexões, inspecione o conteúdo da primeira página da tabela com o pageinspect:
    [[local]:5432] postgres@postgres=# CREATE EXTENSION pageinspect;
    [[local]:5432] postgres@postgres=# SELECT * FROM heap_page_items(get_raw_page('tabela', 0));