Transações em globais da InterSystems IRIS

Transações em globais da InterSystems IRISO SGBD InterSystems IRIS suporta estruturas interessantes para armazenamento de dados - globais. Essencialmente, estas são chaves de vários níveis com vários benefícios adicionais na forma de transações, funções rápidas para percorrer árvores de dados, bloqueios e sua própria linguagem ObjectScript.

Leia mais sobre globais na série de artigos “Globals são espadas de tesouro para armazenamento de dados”:

Árvores. Parte 1
Árvores. Parte 2
Matrizes esparsas. Parte 3

Fiquei interessado em saber como as transações são implementadas em globais, quais recursos existem. Afinal, esta é uma estrutura de armazenamento de dados completamente diferente das tabelas usuais. Nível muito inferior.

Como é sabido pela teoria dos bancos de dados relacionais, uma boa implementação de transações deve satisfazer os requisitos ACID:

A - Atômico (atomicidade). Todas as alterações feitas na transação ou nenhuma delas são registradas.

C - Consistência. Após a conclusão de uma transação, o estado lógico do banco de dados deve ser internamente consistente. Em muitos aspectos, esse requisito diz respeito ao programador, mas no caso de bancos de dados SQL também diz respeito a chaves estrangeiras.

Eu - Isolar. As transações executadas em paralelo não devem afetar umas às outras.

D - Durável. Após a conclusão bem-sucedida de uma transação, problemas em níveis inferiores (falta de energia, por exemplo) não deverão afetar os dados alterados pela transação.

Globais são estruturas de dados não relacionais. Eles foram projetados para rodar super rápido em hardware muito limitado. Vejamos a implementação de transações em globais usando imagem oficial do docker IRIS.

Para suportar transações no IRIS, os seguintes comandos são usados: TSTART, COMPROMISSO, TROLLBACK.

1. Atomicidade

A maneira mais fácil de verificar é a atomicidade. Verificamos no console do banco de dados.

Kill ^a
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3
TCOMMIT

Então concluímos:

Write ^a(1), “ ”, ^a(2), “ ”, ^a(3)

Temos:

1 2 3

Tudo está bem. A atomicidade é mantida: todas as alterações são registradas.

Vamos complicar a tarefa, introduzir um erro e ver como a transação é salva, parcialmente ou não.

Vamos verificar a atomicidade novamente:

Kill ^A
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3

Então vamos parar o contêiner à força, iniciá-lo e ver.

docker kill my-iris

Este comando é quase equivalente a um desligamento forçado, pois envia um sinal SIGKILL para interromper o processo imediatamente.

Talvez a transação tenha sido parcialmente salva?

WRITE ^a(1), ^a(2), ^a(3)
^
<UNDEFINED> ^a(1)

- Não, não sobreviveu.

Vamos tentar o comando de reversão:

Kill ^A
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3
TROLLBACK

WRITE ^a(1), ^a(2), ^a(3)
^
<UNDEFINED> ^a(1)

Nada sobreviveu também.

2. Consistência

Como em bancos de dados baseados em globais, as chaves também são feitas em globais (deixe-me lembrar que global é uma estrutura de armazenamento de dados de nível inferior do que uma tabela relacional), para atender ao requisito de consistência, uma alteração na chave deve ser incluída na mesma transação que uma mudança no global.

Por exemplo, temos um ^person global, no qual armazenamos personalidades e usamos o TIN como chave.

^person(1234567, ‘firstname’) = ‘Sergey’
^person(1234567, ‘lastname’) = ‘Kamenev’
^person(1234567, ‘phone’) = ‘+74995555555
...

Para fazer uma busca rápida por sobrenome e nome, criamos a chave ^index.

^index(‘Kamenev’, ‘Sergey’, 1234567) = 1

Para que o banco de dados seja consistente, devemos adicionar a persona assim:

TSTART
^person(1234567, ‘firstname’) = ‘Sergey’
^person(1234567, ‘lastname’) = ‘Kamenev’
^person(1234567, ‘phone’) = ‘+74995555555
^index(‘Kamenev’, ‘Sergey’, 1234567) = 1
TCOMMIT

Assim, ao excluir também devemos usar uma transação:

TSTART
Kill ^person(1234567)
ZKill ^index(‘Kamenev’, ‘Sergey’, 1234567)
TCOMMIT

Em outras palavras, o cumprimento do requisito de consistência depende inteiramente dos ombros do programador. Mas quando se trata de globais, isso é normal, devido à sua natureza de baixo nível.

3. Isolamento

É aqui que a selva começa. Muitos usuários trabalham simultaneamente no mesmo banco de dados, alterando os mesmos dados.

A situação é comparável a quando muitos usuários trabalham simultaneamente com o mesmo repositório de código e tentam submeter alterações simultaneamente em vários arquivos de uma só vez.

O banco de dados deve resolver tudo em tempo real. Considerando que em empresas sérias existe até uma pessoa especial responsável pelo controle de versão (para mesclar filiais, resolver conflitos, etc.), e o banco de dados deve fazer tudo isso em tempo real, a complexidade da tarefa e a correção do design de banco de dados e código que o atende.

O banco de dados não consegue compreender o significado das ações realizadas pelos usuários para evitar conflitos caso estejam trabalhando nos mesmos dados. Ele só pode desfazer uma transação que esteja em conflito com outra ou executá-las sequencialmente.

Outro problema é que durante a execução de uma transação (antes de um commit), o estado do banco de dados pode ser inconsistente, por isso é desejável que outras transações não tenham acesso ao estado inconsistente do banco de dados, o que é alcançado em bancos de dados relacionais de várias maneiras: criando instantâneos, linhas multi-versionadas e etc.

Ao executar transações em paralelo, é importante para nós que elas não interfiram umas nas outras. Esta é a propriedade do isolamento.

SQL define 4 níveis de isolamento:

  • LEIA NÃO COMPROMETIDA
  • LEIA COMPROMETIDO
  • REPEATABLE READ
  • SERIALIZÁVEL

Vejamos cada nível separadamente. Os custos de implementação de cada nível crescem quase exponencialmente.

LEIA NÃO COMPROMETIDA - este é o nível de isolamento mais baixo, mas ao mesmo tempo o mais rápido. As transações podem ler alterações feitas umas pelas outras.

LEIA COMPROMETIDO é o próximo nível de isolamento, que é um compromisso. As transações não podem ler as alterações umas das outras antes do commit, mas podem ler quaisquer alterações feitas após o commit.

Se tivermos uma transação T1 longa, durante a qual ocorreram commits nas transações T2, T3...Tn, que funcionavam com os mesmos dados que T1, então ao solicitar dados em T1 obteremos um resultado diferente a cada vez. Este fenômeno é chamado de leitura não repetível.

REPEATABLE READ — neste nível de isolamento não temos o fenômeno da leitura não repetível, devido ao fato de que para cada solicitação de leitura de dados é criado um snapshot dos dados resultantes e quando reutilizados na mesma transação, os dados do snapshot é usado. No entanto, é possível ler dados fantasmas neste nível de isolamento. Isso se refere à leitura de novas linhas que foram adicionadas por transações confirmadas paralelamente.

SERIALIZÁVEL — o mais alto nível de isolamento. Caracteriza-se pelo fato de que os dados utilizados de alguma forma em uma transação (leitura ou alteração) ficam disponíveis para outras transações somente após a conclusão da primeira transação.

Primeiro, vamos descobrir se há isolamento de operações em uma transação do thread principal. Vamos abrir 2 janelas de terminal.

Kill ^t

Write ^t(1)
2

TSTART
Set ^t(1)=2

Não há isolamento. Um thread vê o que o segundo que abriu a transação está fazendo.

Vamos ver se as transações de threads diferentes veem o que está acontecendo dentro delas.

Vamos abrir 2 janelas de terminal e 2 transações em paralelo.

kill ^t
TSTART
Write ^t(1)
3

TSTART
Set ^t(1)=3

As transações paralelas veem os dados umas das outras. Então, obtivemos o nível de isolamento mais simples, mas também o mais rápido, LEIA NÃO COMPROMETIDO.

Em princípio, isto poderia ser esperado para os globais, para os quais o desempenho sempre foi uma prioridade.

E se precisarmos de um nível mais elevado de isolamento nas operações globais?

Aqui você precisa pensar sobre por que os níveis de isolamento são necessários e como eles funcionam.

O nível de isolamento mais alto, SERIALIZE, significa que o resultado das transações executadas em paralelo é equivalente à sua execução sequencial, o que garante a ausência de colisões.

Podemos fazer isso usando bloqueios inteligentes no ObjectScript, que têm muitos usos diferentes: você pode fazer bloqueios regulares, incrementais e múltiplos com o comando BLOQUEIO.

Níveis de isolamento mais baixos são compensações projetadas para aumentar a velocidade do banco de dados.

Vamos ver como podemos alcançar diferentes níveis de isolamento usando bloqueios.

Este operador permite tirar não apenas bloqueios exclusivos necessários para alterar dados, mas os chamados bloqueios compartilhados, que podem levar vários threads em paralelo quando precisam ler dados que não devem ser alterados por outros processos durante o processo de leitura.

Mais informações sobre o método de bloqueio de duas fases em russo e inglês:

Bloqueio bifásico
Bloqueio de duas fases

A dificuldade é que durante uma transação o estado do banco de dados pode ser inconsistente, mas esses dados inconsistentes são visíveis para outros processos. Como evitar isso?

Usando bloqueios, criaremos janelas de visibilidade nas quais o estado do banco de dados será consistente. E todo o acesso a tais janelas de visibilidade do estado acordado será controlado por fechaduras.

Os bloqueios compartilhados nos mesmos dados são reutilizáveis ​​– vários processos podem utilizá-los. Esses bloqueios evitam que outros processos alterem dados, ou seja, eles são usados ​​para formar janelas de estado consistente do banco de dados.

Bloqueios exclusivos são usados ​​para alterações de dados - apenas um processo pode receber tal bloqueio. Um bloqueio exclusivo pode ser obtido por:

  1. Qualquer processo se os dados forem gratuitos
  2. Somente o processo que possui bloqueio compartilhado nesses dados e foi o primeiro a solicitar bloqueio exclusivo.

Transações em globais da InterSystems IRIS

Quanto mais estreita a janela de visibilidade, mais tempo os outros processos terão que esperar por ela, mas mais consistente poderá ser o estado do banco de dados dentro dela.

READ_COMMITTED — a essência deste nível é que vemos apenas dados confirmados de outros threads. Se os dados de outra transação ainda não foram confirmados, veremos sua versão antiga.

Isso nos permite paralelizar o trabalho em vez de esperar que o bloqueio seja liberado.

Sem truques especiais, não poderemos ver a versão antiga dos dados no IRIS, então teremos que nos contentar com bloqueios.

Conseqüentemente, teremos que usar bloqueios compartilhados para permitir que os dados sejam lidos apenas em momentos de consistência.

Digamos que temos uma base de usuários que transferem dinheiro entre si.

Momento da transferência da pessoa 123 para a pessoa 242:

LOCK +^person(123), +^person(242)
Set ^person(123, amount) = ^person(123, amount) - amount
Set ^person(242, amount) = ^person(242, amount) + amount
LOCK -^person(123), -^person(242)

O momento de solicitar o valor em dinheiro à pessoa 123 antes do débito deve ser acompanhado de um bloqueio exclusivo (por padrão):

LOCK +^person(123)
Write ^person(123)

E se precisar mostrar o status da conta em sua conta pessoal, você pode usar um bloqueio compartilhado ou nem mesmo usá-lo:

LOCK +^person(123)#”S”
Write ^person(123)

No entanto, se assumirmos que as operações de banco de dados são executadas quase instantaneamente (deixe-me lembrá-lo de que as globais são uma estrutura de nível muito inferior a uma tabela relacional), a necessidade desse nível diminui.

REPEATABLE READ - Este nível de isolamento permite múltiplas leituras de dados que podem ser modificados por transações simultâneas.

Assim, teremos que colocar um bloqueio compartilhado na leitura dos dados que alteramos e bloqueios exclusivos nos dados que alteramos.

Felizmente, o operador LOCK permite listar detalhadamente todos os bloqueios necessários, dos quais pode haver muitos, em uma única instrução.

LOCK +^person(123, amount)#”S”
чтение ^person(123, amount)

outras operações (neste momento, threads paralelos tentam alterar ^person(123, amount), mas não conseguem)

LOCK +^person(123, amount)
изменение ^person(123, amount)
LOCK -^person(123, amount)

чтение ^person(123, amount)
LOCK -^person(123, amount)#”S”

Ao listar os bloqueios separados por vírgulas, eles são obtidos sequencialmente, mas se você fizer isso:

LOCK +(^person(123),^person(242))

então eles são tomados atomicamente de uma só vez.

SERIALIZAR — teremos que definir bloqueios para que, em última análise, todas as transações que possuem dados comuns sejam executadas sequencialmente. Para esta abordagem, a maioria dos bloqueios devem ser exclusivos e utilizados nas menores áreas do mundo para desempenho.

Se falamos em débito de fundos no ^person global, então apenas o nível de isolamento SERIALIZE é aceitável para isso, pois o dinheiro deve ser gasto estritamente sequencialmente, caso contrário é possível gastar o mesmo valor várias vezes.

4. Durabilidade

Realizei testes com corte rígido do recipiente usando

docker kill my-iris

A base os tolerou bem. Nenhum problema foi identificado.

Conclusão

Para globais, o InterSystems IRIS oferece suporte a transações. Eles são verdadeiramente atômicos e confiáveis. Para garantir a consistência de um banco de dados baseado em globais, são necessários esforços do programador e o uso de transações, uma vez que não possui construções complexas integradas, como chaves estrangeiras.

O nível de isolamento dos globais sem o uso de bloqueios é READ UNCOMMITED, e ao usar bloqueios pode ser garantido até o nível SERIALIZE.

A exatidão e a velocidade das transações em globais dependem muito da habilidade do programador: quanto mais bloqueios compartilhados forem usados ​​durante a leitura, quanto maior o nível de isolamento, e quanto mais bloqueios exclusivos forem usados, mais rápido será o desempenho.

Fonte: habr.com

Adicionar um comentário