MVCC no PostgreSQL-4. Instantâneos

Depois de discutir os problemas de isolamento e fazer uma digressão em relação à estrutura de dados de baixo nível , na última vez que exploramos as versões de linha e observamos como as diferentes operações alteraram os campos do cabeçalho da tupla.

Agora, veremos como os snapshots de dados consistentes são obtidos das tuplas.

O que é um instantâneo de dados?


As páginas de dados podem conter fisicamente várias versões da mesma linha. Mas cada transação deve ver apenas uma (ou nenhuma) versão de cada linha, para que todas elas formem uma imagem consistente dos dados (no sentido de ACID) em um determinado momento.

O isolamento no PosgreSQL é baseado em instantâneos: cada transação trabalha com seu próprio instantâneo de dados, que "contém" dados confirmados antes do momento em que o instantâneo foi criado e não "contém" dados que ainda não foram confirmados naquele momento. Já vimos que, embora o isolamento resultante pareça mais rígido do que o exigido pelo padrão, ele ainda apresenta anomalias.

No nível de isolamento Read Committed, um instantâneo é criado no início de cada instrução de transação. Este instantâneo está ativo enquanto a instrução está sendo executada. Na figura, o momento em que o instantâneo foi criado (que, como lembramos, é determinado pelo ID da transação) é mostrado em azul.



Nos níveis Leitura Repetível e Serializável, o instantâneo é criado uma vez, no início da primeira instrução de transação. Esse instantâneo permanece ativo até o final da transação.



Visibilidade das tuplas em um instantâneo


Regras de visibilidade


Um instantâneo certamente não é uma cópia física de todas as tuplas necessárias. Na verdade, um instantâneo é especificado por vários números e a visibilidade das tuplas em um instantâneo é determinada por regras.

Se uma tupla estará visível ou não em uma captura instantânea depende de dois campos no cabeçalho, ou seja, xmin e xmax , ou seja, os IDs das transações que criaram e excluíram a tupla. Intervalos como esse não se sobrepõem e, portanto, não mais de uma versão representa uma linha em cada instantâneo.

As regras exatas de visibilidade são bastante complicadas e levam em consideração muitos casos e extremos diferentes.
Você pode garantir isso facilmente, consultando src / backend / utils / time / tqual.c (na versão 12, a verificação foi movida para src / backend / access / heap / heapam_visibility.c).

Para simplificar, podemos dizer que uma tupla é visível quando, no instantâneo, as alterações feitas pela transação xmin são visíveis, enquanto as feitas pela transação xmax não são (em outras palavras, já está claro que a tupla foi criada, mas ainda não está claro se foi excluído).

Em relação a uma transação, suas alterações são visíveis no instantâneo, se é a própria transação que criou o instantâneo (ele vê suas próprias alterações ainda não confirmadas) ou a transação foi confirmada antes da criação do instantâneo.

Podemos representar graficamente transações por segmentos (desde o horário de início até o horário de confirmação):



Aqui:

  • As alterações da transação 2 serão visíveis desde que foram concluídas antes da criação do instantâneo.
  • As alterações da transação 1 não serão visíveis, pois estavam ativas no momento em que o instantâneo foi criado.
  • As alterações da transação 3 não serão visíveis desde que foram iniciadas após a criação do instantâneo (independentemente de ter sido concluído ou não).

Infelizmente, o sistema desconhece o tempo de consolidação das transações. Somente sua hora de início é conhecida (que é determinada pelo ID da transação e marcada com uma linha tracejada nas figuras acima), mas o evento de conclusão não é gravado em nenhum lugar.

Tudo o que podemos fazer é descobrir o status atual das transações na criação do instantâneo. Esta informação está disponível na memória compartilhada do servidor, na estrutura ProcArray, que contém a lista de todas as sessões ativas e suas transações.

Mas não seremos capazes de descobrir após o fato se uma determinada transação estava ativa ou não no momento em que o instantâneo foi criado. Portanto, um instantâneo deve armazenar uma lista de todas as transações ativas atuais.

Pelo exposto, segue-se que no PostgreSQL, não é possível criar uma captura instantânea que mostre dados consistentes a partir de um certo tempo, mesmo se todas as tuplas necessárias estiverem disponíveis nas páginas da tabela. Muitas vezes, surge uma pergunta por que o PostgreSQL não possui consultas retrospectivas (ou temporais; ou flashback, como a Oracle as chama) - e esse é um dos motivos.
O engraçado é que essa funcionalidade foi disponibilizada primeiro, mas depois excluída do DBMS. Você pode ler sobre isso no artigo de Joseph M. Hellerstein .

Portanto, o instantâneo é determinado por vários parâmetros:

  • No momento em que o instantâneo foi criado, mais exatamente, o ID da próxima transação, ainda indisponível no sistema ( snapshot.xmax ).
  • A lista de transações ativas (em andamento) no momento em que a captura instantânea foi criada ( snapshot.xip ).

Por conveniência e otimização, o ID da transação ativa mais antiga também é armazenado ( snapshot.xmin ). Esse valor faz um sentido importante, que será discutido abaixo.

O instantâneo também armazena mais alguns parâmetros, que não são importantes para nós.



Exemplo


Para entender como o instantâneo determina a visibilidade, vamos reproduzir o exemplo acima com três transações. A tabela terá três linhas, onde:

  • O primeiro foi adicionado por uma transação iniciada antes da criação da captura instantânea, mas concluída após ela.
  • O segundo foi adicionado por uma transação iniciada e concluída antes da criação da captura instantânea.
  • O terceiro foi adicionado após a criação do instantâneo.

 => TRUNCATE TABLE accounts; 

A primeira transação (ainda não concluída):

 => BEGIN; => INSERT INTO accounts VALUES (1, '1001', 'alice', 1000.00); => SELECT txid_current(); 
 => SELECT txid_current(); txid_current -------------- 3695 (1 row) 

A segunda transação (concluída antes da criação do instantâneo):

 | => BEGIN; | => INSERT INTO accounts VALUES (2, '2001', 'bob', 100.00); | => SELECT txid_current(); 
 | txid_current | -------------- | 3696 | (1 row) 
 | => COMMIT; 

Criando um instantâneo em uma transação em outra sessão.

 || => BEGIN ISOLATION LEVEL REPEATABLE READ; || => SELECT xmin, xmax, * FROM accounts; 
 || xmin | xmax | id | number | client | amount || ------+------+----+--------+--------+-------- || 3696 | 0 | 2 | 2001 | bob | 100.00 || (1 row) 

Confirmando a primeira transação após a criação da captura instantânea:

 => COMMIT; 

E a terceira transação (apareceu após a criação do instantâneo):

 | => BEGIN; | => INSERT INTO accounts VALUES (3, '2002', 'bob', 900.00); | => SELECT txid_current(); 
 | txid_current | -------------- | 3697 | (1 row) 
 | => COMMIT; 

Evidentemente, apenas uma linha ainda é visível em nosso instantâneo:

 || => SELECT xmin, xmax, * FROM accounts; 
 || xmin | xmax | id | number | client | amount || ------+------+----+--------+--------+-------- || 3696 | 0 | 2 | 2001 | bob | 100.00 || (1 row) 

A questão é como o Postgres entende isso.

Tudo é determinado pelo instantâneo. Vejamos:

 || => SELECT txid_current_snapshot(); 
 || txid_current_snapshot || ----------------------- || 3695:3697:3695 || (1 row) 

Aqui snapshot.xmin , snapshot.xmax e snapshot.xip são listados, delimitados por dois pontos (nesse caso, snapshot.xip é um número, mas em geral é uma lista).

De acordo com as regras acima, no instantâneo, essas alterações devem ser visíveis pelas transações com os IDs xid modo que snapshot.xmin <= xid < snapshot.xmax exceto aqueles que estão na lista snapshot.xip . Vejamos todas as linhas da tabela (no novo instantâneo):

 => SELECT xmin, xmax, * FROM accounts ORDER BY id; 
  xmin | xmax | id | number | client | amount ------+------+----+--------+--------+--------- 3695 | 0 | 1 | 1001 | alice | 1000.00 3696 | 0 | 2 | 2001 | bob | 100.00 3697 | 0 | 3 | 2002 | bob | 900.00 (3 rows) 

A primeira linha não está visível: foi criada por uma transação que está na lista de transações ativas ( xip ).
A segunda linha é visível: foi criada por uma transação que está no intervalo da captura instantânea.
A terceira linha não está visível: foi criada por uma transação que está fora do intervalo da captura instantânea.

 || => COMMIT; 

Mudanças da própria transação


Determinar a visibilidade das próprias alterações da transação complica um pouco a situação. Nesse caso, pode ser necessário ver apenas parte dessas alterações. Por exemplo: em qualquer nível de isolamento, um cursor aberto em um determinado momento não deve ver as alterações feitas posteriormente.

Para esse fim, um cabeçalho de tupla possui um campo especial (representado nas pseudo-colunas cmin e cmax ), que mostra o número do pedido dentro da transação. cmin é o número para inserção e cmax - para exclusão, mas para economizar espaço no cabeçalho da tupla, esse é realmente um campo em vez de dois campos diferentes. Supõe-se que uma transação raramente insira e exclua a mesma linha.

Mas se isso acontecer, um ID de comando de combinação especial ( combocid ) será inserido no mesmo campo, e o processo de back-end lembrará o cmin e cmin reais para esse combocid . Mas isso é totalmente exótico.

Aqui está um exemplo simples. Vamos iniciar uma transação e adicionar uma linha à tabela:

 => BEGIN; => SELECT txid_current(); 
  txid_current -------------- 3698 (1 row) 
 INSERT INTO accounts(id, number, client, amount) VALUES (4, 3001, 'charlie', 100.00); 

Vamos exibir o conteúdo da tabela, junto com o campo cmin (mas apenas para linhas adicionadas pela transação - para outras, não faz sentido):

 => SELECT xmin, CASE WHEN xmin = 3698 THEN cmin END cmin, * FROM accounts; 
  xmin | cmin | id | number | client | amount ------+------+----+--------+---------+--------- 3695 | | 1 | 1001 | alice | 1000.00 3696 | | 2 | 2001 | bob | 100.00 3697 | | 3 | 2002 | bob | 900.00 3698 | 0 | 4 | 3001 | charlie | 100.00 (4 rows) 

Agora, abrimos um cursor para uma consulta que retorna o número de linhas na tabela.

 => DECLARE c CURSOR FOR SELECT count(*) FROM accounts; 

E depois adicionamos outra linha:

 => INSERT INTO accounts(id, number, client, amount) VALUES (5, 3002, 'charlie', 200.00); 

A consulta retorna 4 - a linha adicionada após abrir o cursor não entra no instantâneo de dados:

 => FETCH c; 
  count ------- 4 (1 row) 

Porque Como o instantâneo leva em consideração apenas as tuplas com cmin < 1 .

 => SELECT xmin, CASE WHEN xmin = 3698 THEN cmin END cmin, * FROM accounts; 
  xmin | cmin | id | number | client | amount ------+------+----+--------+---------+--------- 3695 | | 1 | 1001 | alice | 1000.00 3696 | | 2 | 2001 | bob | 100.00 3697 | | 3 | 2002 | bob | 900.00 3698 | 0 | 4 | 3001 | charlie | 100.00 3698 | 1 | 5 | 3002 | charlie | 200.00 (5 rows) 
 => ROLLBACK; 

Horizonte de eventos


O ID da transação ativa mais antiga ( snapshot.xmin ) faz um sentido importante: determina o "horizonte de eventos" da transação. Ou seja, além do seu horizonte, a transação sempre vê apenas versões de linha atualizadas.

Realmente, uma versão de linha desatualizada (inativa) precisa estar visível apenas quando a versão atualizada foi criada por uma transação ainda não concluída e, portanto, ainda não está visível. Mas todas as transações "além do horizonte" são concluídas com certeza.



Você pode ver o horizonte de transações no catálogo do sistema:

 => BEGIN; => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid(); 
  backend_xmin -------------- 3699 (1 row) 

Também podemos definir o horizonte no nível do banco de dados. Para fazer isso, precisamos tirar todos os instantâneos ativos e encontrar o xmin mais antigo entre eles. E definirá o horizonte, além do qual as tuplas mortas no banco de dados nunca serão visíveis para nenhuma transação. Tais tuplas podem ser aspiradas - e é exatamente por isso que o conceito de horizonte é tão importante do ponto de vista prático.

Se uma determinada transação estiver mantendo um instantâneo por um longo tempo, também manterá o horizonte do banco de dados. Além disso, apenas a existência de uma transação incompleta manterá o horizonte, mesmo que a transação em si não mantenha o instantâneo.

E isso significa que as tuplas mortas no DB não podem ser aspiradas. Além disso, é possível que uma transação de "reprodução longa" não cruze os dados com outras transações, mas isso realmente não importa, pois todos compartilham um horizonte de banco de dados.

Se agora fizermos um segmento representar snapshots (de snapshot.xmin a snapshot.xmax ) em vez de transações, poderemos visualizar a situação da seguinte maneira:



Nesta figura, o instantâneo mais baixo pertence a uma transação incompleta e, nos outros instantâneos, o snapshot.xmin não pode ser maior que o ID da transação.

Em nosso exemplo, a transação foi iniciada com o nível de isolamento Read Committed. Mesmo que não tenha nenhum instantâneo de dados ativo, ele continua mantendo o horizonte:

 | => BEGIN; | => UPDATE accounts SET amount = amount + 1.00; | => COMMIT; 
 => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid(); 
  backend_xmin -------------- 3699 (1 row) 

E somente após a conclusão da transação, o horizonte avança, o que permite aspirar as tuplas mortas de distância:

 => COMMIT; => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid(); 
  backend_xmin -------------- 3700 (1 row) 

Caso a situação descrita realmente cause problemas e não haja como contornar o problema no nível do aplicativo, dois parâmetros estarão disponíveis a partir da versão 9.6:

  • old_snapshot_threshold determina a vida útil máxima da captura instantânea. Quando esse tempo terminar, o servidor estará qualificado para aspirar tuplas mortas e, se uma transação de "reprodução prolongada" ainda precisar delas, receberá um erro "instantâneo muito antigo".
  • idle_in_transaction_session_timeout determina o tempo de vida máximo de uma transação inativa. Quando esse tempo termina, a transação é interrompida.

Exportação de instantâneo


Às vezes, surgem situações em que várias transações simultâneas devem ser garantidas para ver os mesmos dados. Um exemplo é um utilitário pg_dump , que pode funcionar em modo paralelo: todos os processos de trabalho devem ver o banco de dados no mesmo estado para que a cópia de backup seja consistente.

Obviamente, não podemos confiar na crença de que as transações verão os mesmos dados apenas porque foram iniciadas "simultaneamente". Para esse fim, a exportação e a importação de um instantâneo estão disponíveis.

A função pg_export_snapshot retorna o ID da captura instantânea, que pode ser passada para outra transação (usando ferramentas fora do DBMS).

 => BEGIN ISOLATION LEVEL REPEATABLE READ; => SELECT count(*) FROM accounts; -- any query 
  count ------- 3 (1 row) 
 => SELECT pg_export_snapshot(); 
  pg_export_snapshot --------------------- 00000004-00000E7B-1 (1 row) 

A outra transação pode importar a captura instantânea usando o comando SET TRANSACTION SNAPSHOT antes de executar sua primeira consulta. O nível de isolamento Leitura Repetível ou Serializável também deve ser especificado antes, pois no nível Leitura Confirmada, as instruções usarão seus próprios instantâneos.

 | => DELETE FROM accounts; | => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SET TRANSACTION SNAPSHOT '00000004-00000E7B-1'; 

A segunda transação agora funcionará com o instantâneo da primeira e, portanto, verá três linhas (em vez de zero):

 | => SELECT count(*) FROM accounts; 
 | count | ------- | 3 | (1 row) 

A vida útil de uma captura instantânea exportada é igual à vida útil da transação de exportação.

 | => COMMIT; => COMMIT; 

Continue lendo .

Source: https://habr.com/ru/post/pt479512/


All Articles