NoSQL e Atomicidade

Mateus Forgiarini da Silva
CWI Software
Published in
5 min readJan 15, 2018

Resolvi escrever este artigo porque percebi que existe muito desconhecimento sobre como deve-se modelar bancos de dados não relacionais orientados a aggregates (caso de document, key-value e column databases), principalmente de uma maneira que se garanta a consistência de dados, pois uma das coisas que sempre escuto é que bancos NoSQL não suportam atomic transactions. Em virtude deste fato, irei mostrar que não é bem assim que a coisa funciona.

Primeiramente, um aggregate, termo usado no Domain-Driven Design, nada mais é que um conjunto de objetos que são tratados como uma unidade. Se você vem do mundo relacional, deve estar acostumado com o modelo pelo qual o banco de dados organiza os seus dados internamente. Onde você tem uma tabela, com suas colunas e linhas, estas últimas conhecidas formalmente como tuples. Diferentemente do mundo NoSQL, onde trabalhamos com objetos complexos, uma tuple é uma estrutura limitada de dados que não pode conter, por exemplo, tuples aninadas. Bancos não relacionais, suportam o armazenamento de objetos complexos e por serem, em sua maioria, desenhados para rodar em clusters, tornam-se o caso ideal para o uso de aggregates bem definidos.

Um cluster nada mais é que uma arquitetura onde o armazenamento de dados é alocado em vários nodes, que são compostos por uma estrutura dividida em in-memory, hard disk e processador. Ao especificar através de aggregates quais bits de dados serão armazenados em um mesmo node, acabamos por minimizar o número de interações entre diferentes nodes, o que acaba nos dando um ganho de performance, fato pelo qual este tipo de arquitetura se diferencia e é usada. Por esta breve descrição da arquitetura de um banco NoSQL, você já pode ter uma ideia do porquê é interessante manipular dados em formas de aggregates.

Mas eu quero um exemplo:

Pois bem, digamos que eu esteja desenhando um sistema para restaurantes para que meus empregados consigam atender mais rapidamente os clientes.

Para fazer um pedido, um garçom terá que preencher os dados do cliente, o nome no caso, a mesa do pedido, e os pedidos em si. Assim, podemos pensar em pelo menos duas maneiras de armazenar esses dados.

Primeiramente, vamos analisar uma situação onde o desenvolvedor prefere dividir a situação em dois aggregates, um para o cliente e outro para o pedido.

No mundo NoSQL esta não seria uma situação onde garantiríamos a atomicidade da transação.

Imagine o seguinte, digamos que o cliente é salvo em um node, e os pedidos em outro. O que aconteceria se ocorresse um erro de transação enquanto estivéssemos salvando os dois aggregates? Se o nosso banco de dados não suportar transações atômicas com mais de um aggregate (sim, existem bancos não relacionais que suportam transações atômicas com múltiplos aggregates, como o Firestore do Firebase), poderíamos acabar com um aggregate sendo salvo e outro não. Assim, poderíamos acabar com um pedido que não tem cliente. Desta forma, teríamos uma inconsistência de dados. É bem verdade que esse não seria um grande problema neste caso, pois o pedido seria entregue na mesa correta. No entanto, teríamos um banco de dados inconsistente com um pedido fazendo referência a um cliente que não existe.

Este é o principal argumento de que quem desconhece o funcionamento de um banco não relacional, ou seja, que não é possível dar um rollback caso ocorra um erro de transação e que por isso teríamos uma grande chance de ter um banco de dados inconsistente (o que às vezes não é um problema, dependendo da regra de negócio).

Porém, pense comigo. Se nós salvássemos os nossos dados em apenas um aggregate, esse problema estaria resolvido, certo?

Sim, os dados estariam encapsulados em um único aggregate, de forma que se ocorresse um erro de transação, nosso banco de dados não ficaria inconsistente, pois estamos trabalhando com um aggregate, então toda a informação que precisamos está alocada em um mesmo objeto. Assim não seria possível ter, por exemplo, um pedido sem cliente.

Claro que isso depende muito de como a arquitetura de seu banco de dados foi desenhada. Por exemplo, este conceito não se aplica em uma arquitetura de peer-to-peer replication, onde você não usa um único node para salvar um tipo de dado em específico, mas sim vários nodes. Ou em um sistema de master-slave onde você tem mais de um master, portanto mais de um node executando um mesmo tipo de write. Além disso, deve-se levar em conta o fato de que muitas vezes é difícil especificar o boundary de um aggregate. Por exemplo, o que aconteceria em nosso exemplo se quisemos saber o número de Sushis vendidos durante o mês de janeiro? Teríamos que inspecionar cada clienteEPedido para recuperar essa informação, por esse motivo bancos relacionais tornam-se mais interessantes quando não sabemos como iremos acessar os dados, pois podemos executar joins e recuperar os dados de diferentes maneiras (apesar do custo de performance que muitos joins podem causar).

No entanto, esses poréns não invalidam o fato de que que bancos NoSQL suportam transações atômicas de um ÚNICO aggregate por vez. Visto isso é importante ter em mente que é necessário minimizar as transações entre aggregates no mundo NoSQL, não só por um questão de performance, mas também porque quando trabalhamos com um aggregate por transação, nós garantimos a atomicidade do banco de dados. Ou seja, o problema de atomicidade dos bancos não relacionais que muito se escuta nas discussões, é mais um problema do código que foi feito sem pensar na arquitetura do banco de dados, do que do banco de dados em si.Por fim, eu deixo uma frase de Martin Fowler que demonstra bem o que eu tentei passar neste artigo e serve de reflexão para nós desenvolvedores.

“If we need to manipulate multiple aggregates in an atomic way, we have to manage that ourselves in the application code.”

Martin Fowler.

--

--