Por que eu devo ler este artigo:Neste artigo será feita uma introdução ao banco de dados NoSQL Cassandra. Serão descritos aspectos gerais da arquitetura, principais vantagens e desvantagens e como ele se comporta comparado aos bancos de dados relacionais. Ao passo que os sistemas de informação geram dados com volume cada vez maior e a velocidades crescentes, é importante pensar em uma arquitetura de armazenamento de dados que seja altamente escalável e performática. Com o Cassandra é possível construir um banco de dados totalmente distribuído que atende às mais exigentes demandas.

Bancos NoSQL têm ganhado cada vez mais popularidade nos últimos tempos, podemos ver no ranking da Figura 1 que entre os dez SGBDs mais populares, temos bancos como o MongoDB e o Cassandra, que estão subindo cada vez mais na lista.

Top 10 SGBDs em popularidade
Figura 1. Top 10 SGBDs em popularidade

Porque esses bancos estão se tornando mais populares e outros, como o Oracle, tem perdido um grande volume de adesão? Cada vez mais temos sistemas que precisam trabalhar com volumes altíssimos de transações por segundo e os bancos de dados precisam estar preparados para aguentar a demanda. Um exemplo de um cenário de alta demanda são as Black-Fridays, que todo ano tem batido recordes de vendas no Brasil. Montar uma estrutura de banco de dados relacional que atenda à demanda de evento desse tipo exige um alto investimento, contudo, estamos falando de um único dia em que essa demanda é muito acima do normal. Seria conveniente que, passado o momento da Black-Friday, encolher a estrutura de banco de dados de forma que o custo de manutenção seja muito menor, porém isso não é muito simples de ser feito em uma estrutura de banco relacional.

É nesse tipo de cenário que alguns bancos NoSQL tem ganhado popularidade. Eles possuem uma estrutura de dados muito mais flexível e são, por concepção, feitos para trabalhar de forma distribuída. Essas características permitem que eles lidem melhor com altas demandas de processamento além de terem uma grande capacidade de elasticidade, ou seja, em momentos que exigem alta demanda, pode-se alocar um número maior de servidores, após esse período, basta removê-los da estrutura e economizar na infraestrutura.

Neste artigo será feita uma introdução a um dos bancos NoSQL mais populares, o Cassandra, cobrindo suas características e casos de uso mais comuns, além de alguns exemplos de como criar e consultar estruturas de dados básicas dentro dele.

Introdução ao Cassandra

O Cassandra foi inicialmente desenvolvido pelo Facebook para ser utilizado no motor de busca de sua caixa de entrada de mensagens. Em 2008 ele se tornou open-source e em 2009 passou a ser mantido pela Apache Foundation. Seu modelo de distribuição do sistema é baseado no Dynamo (desenvolvido pela Amazon) enquanto a forma de organização dos dados é baseado no BigTable (desenvolvido pelo Google).

O Dynamo nasceu a partir da necessidade de se ter um banco de dados simples, altamente escalável e confiável para lidar com grandes demandas de leitura/escrita. Essa motivação veio de uma série de momentos de indisponibilidade do site da Amazon durante a Black-Friday de 2004 nos Estados Unidos causados em boa parte por sobrecargas nos bancos relacionais utilizados na época, resultando em prejuízo financeiro para o site de e-commerce. O Dynamo então foi desenvolvido e empregado para tratar os sistemas que sofriam com maior demanda como o de carrinho de compras e sessões de usuário. Em 2007, a Amazon disponibilizou um documento descrevendo como a arquitetura do Dynamo funcionava, esse documento serviu de base para a criação de vários outros bancos NoSQL.

O BigTable também começou a ser desenvolvido em 2004 pelo Google, também como uma solução altamente escalável e distribuída. O desafio nesse caso era armazenar o volume imenso de dados de indexação de todas as páginas web mapeadas pelo Google que então era utilizado para alimentar o seu motor de buscas.

A distribuição oficial do Cassandra é compatível com todas as distribuições do Linux e com o Mac OS. Existe também uma versão compatível com o Windows distribuída pela DataStax. O Cassandra é, por concepção, feito para trabalhar de forma distribuída, sendo que não há grandes vantagens em trabalhar com ele utilizando apenas uma máquina. Ao utilizar várias máquinas (também chamadas de nós), vemos o verdadeiro potencial da solução.

Outra característica fundamental da arquitetura é o fato dela ser descentralizada, ou seja, diferente da arquitetura master-slave encontrada em outros sistemas de banco de dados, dessa forma, todos os nós da rede possuem as mesmas funções e capacidades, não há um ponto único de falha. Isso também facilita a manutenção da solução já que não é necessário realizar configurações específicas para cada nó. Os nós também não compartilham entre si nenhum tipo de recurso de hardware como disco, processamento ou memória, esse tipo de arquitetura é conhecido como shared-nothing. Isso evita gargalos no sistema e permite que os nós sejam heterogêneos entre si.

Por ter uma arquitetura distribuída e descentralizada, uma solução com Cassandra é altamente escalável, principalmente para escalar de forma horizontal (conhecida também como escalabilidade elástica). Caso seja necessária mais performance, basta adicionar mais nós na rede, não é necessário a priori substituir as máquinas existentes por servidores mais poderosos (ou escalar verticalmente). Da mesma forma, se a demanda diminuir, pode-se remover alguns nós e economizar na manutenção do sistema. Alie isso a uma infraestrutura baseada em nuvem e é possível atingir um alto grau de eficiência de uso de recursos de hardware.

Os dados das tabelas do Cassandra passam automaticamente por um processo de particionamento utilizando a partition key da tabela que, por padrão, é baseada na chave primária da tabela. As partições são então distribuídas entre os nós do cluster utilizando uma técnica conhecida como data sharding. As partições são replicadas em múltiplos nós para prover alta disponibilidade e tolerância a falhas, o número de réplicas é configurado na definição de um keyspace (o conceito de keyspace será abordado mais adiante neste artigo).

Para determinar em que nó do cluster cada partição será gravada, são definidas faixas baseadas no hash da chave utilizada para particionamento, essas faixas são chamadas de tokens e são representadas por um inteiro de 64 bits (ou seja, números que podem variar de –263 até 263–1). Cada nó assume responsabilidade por um ou mais tokens até cobrirem todas as faixas possíveis. Esse tipo de arquitetura é conhecido como Arquitetura em Anel, como representado pela Figura 2.

Representação da arquitetura em anel
Figura 2. Representação da arquitetura em anel

Para detectar nós com indícios de falha, o Cassandra emprega o gossip protocol: de tempos em tempos os nós do cluster enviam pequenas mensagens entre si, caso algum nó falhe sucessivamente ao responder a essas mensagens, ele é marcado como um nó defeituoso e ações corretivas são disparadas em background como replicar os dados que estavam nesse servidor para outros nós. O Cassandra também é capaz de detectar se um dado retornado por um dos nós está desatualizado, nesse caso, outro nó será consultado em busca da versão mais recente do dado e é disparado um processo secundário para correção do dado desatualizado.

Por se tratar de um sistema distribuído, é importante entender os níveis de consistência ao ler e gravar dados no Cassandra. Ao fazer a inserção de um registro, pode-se informar que os dados devem ser imediatamente replicados entre os nós responsáveis (o que é chamado de consistência forte ou consistência imediata), ou, se a informação não é crítica, pode-se instruir que os dados sejam replicados aos poucos, conforme a disponibilidade do cluster (modo chamado de consistência fraca ou consistência eventual). Esses níveis de consistência também são aplicáveis ao efetuar comandos de leitura. É importante ter em mente que quanto mais forte o nível de consistência, mais oneroso é para o sistema e maior a latência ao executar os comandos.

Estrutura dos dados no Cassandra

O Cassandra é um banco de dados NoSQL. Um banco NoSQL não emprega os conceitos tradicionais de banco de dados relacional. Não há definição de um schema, as tabelas normalmente não possuem definições precisas de colunas e não existe o conceito de constraints como chaves estrangeiras. Porém, isso não impede que sejam utilizadas linguagens parecidas com o SQL para interagir com sistemas NoSQL (às vezes referidos como Not Only SQL).

Existem tipos diferentes de bancos de dados NoSQL, podemos listar os seguintes:

  • Orientado à grafos: bancos baseados na teoria dos grafos, são úteis em cenários como geração de rotas ou redes de elementos com várias interconexões, como redes sociais. O Neo4j é um banco de dados que emprega esse conceito;
  • Armazenamento de chave-valor: um dos tipos mais simples, os valores são armazenados e indexados por meio de uma chave. O Cassandra faz parte dessa categoria;
  • Armazenamento colunar: neste tipo de banco os dados as colunas das tabelas são armazenadas como estruturas de dados separadas atrelados a uma chave primária. Um exemplo dessa categoria é o HBase;
  • Orientado à documentos: similar ao conceito de chave-valor, mas o valor nesse caso é um documento que representa um objeto complexo semiestruturado (normalmente na forma de um JSON). O MongoDB é um banco de dados desse tipo.

Como citado anteriormente, o Cassandra é um banco NoSQL do tipo chave-valor. Cada conjunto de chave-valor é equivalente ao que seria uma coluna e um conjunto de colunas associadas a uma chave primária compõem o que seria uma linha de uma tabela do Cassandra, como representado na Figura 3.

Estrutura de uma linha de uma tabela no Cassandra
Figura 3. Estrutura de uma linha de uma tabela no Cassandra

As tabelas no Cassandra têm um propósito muito similar às tabelas que encontramos em bancos relacionais. Elas têm a função de agrupar dados correspondentes à uma determinada entidade. Podemos, por exemplo, ter uma tabela chamada usuário, que por sua vez pode ter as colunas nome, sobrenome, telefone e email.

Agora, diferente de uma tabela de um banco relacional, no Cassandra não é necessário popular todas as colunas ao criar uma nova linha, de forma que podemos ter “larguras” diferentes para cada uma delas (uma coluna de um banco relacional sempre precisa ser preenchida com algum valor, mesmo que seja um valor nulo). A Figura 4 representa esse conceito.

Representação de uma tabela do Cassandra
Figura 4. Representação de uma tabela do Cassandra

Ao gravar um valor em uma coluna, junto a ele é associado um timestamp indicando o horário exato em que o valor foi gravado. Isso é importante para detectar versões desatualizadas de valores quando o Cassandra comparar o resultado de uma consulta feita em vários nós diferentes.

Recapitulando as estruturas de dados básicas do Cassandra, de baixo para cima temos:

  • Colunas: representam um conjunto de chave/valor;
  • Linhas: conjunto de colunas relacionados à uma chave primária;
  • Tabelas: contêiner para um conjunto de linhas;
  • Keyspace: um conjunto de tabelas.

A linguagem CQL

A linguagem CQL (Cassandra Query Language) é utilizada para interagir com o Cassandra. A vantagem te se trabalhar com o CQL é que ele é próximo do mais tradicional SQL, sendo familiar para quem vem do mundo dos bancos relacionais.

Como no SQL, os comandos no CQL são divididos em três categorias básicas, sendo comandos para:

  • Definição das estruturas de dados;
  • Manipulação dos dados;
  • Consultas.

Entretanto, é importante lembrar que o CQL tem suas características próprias que o diferem do SQL, além de outros tipos de dados. Mais detalhes da linguagem serão apresentados na parte prática deste artigo.

Criando um banco de dados no Cassandra

A seguir será apresentado como criar um banco de dados simples dentro do Cassandra. Para isso, será utilizada a versão 3.7 que é a versão estável mais recente disponível no momento da escrita deste artigo. A primeira coisa a ser feita é criar um keyspace. Pode-se criar um utilizando o comando CREATE KEYSPACE:

CREATE KEYSPACE IF NOT EXISTS "my_keyspace"
  WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};

Nesse comando, estamos criando um novo keyspace chamado my_keyspace. Estamos especificando também como deverá ser tratada a replicação dos dados por meio do parâmetro replication, que, por sua vez, possui duas propriedades, sendo:

  • class: indica qual o método de replicação será utilizando para distribuir as réplicas das partições, podendo ser:
    o SimpleStrategy: é o método padrão. Assume que o cluster é composto de um único data center e irá criar um número de réplicas de cada partição das tabelas desse keyspace igual ao valor especificado no parâmetro replication_factor;
    o NetworkTopologyStrategy: esse método permite que seja definido um fator de replicação para cada data center que faz parte do cluster. Nesse caso, o parâmetro replication_factor não é utilizado, sendo necessário passar um conjunto de chaves e valores em que a chave é o nome do data center e o valor é a quantidade de réplicas para o mesmo.
  • replication_factor: indica quantos nós do cluster devem conter uma cópia de cada partição das tabelas deste keyspace caso o método de replicação seja o SimpleStrategy.

Tendo o keyspace criado, o próximo passo é adicionar tabelas a ele. A Listagem 1 cria uma tabela chamada contacts.

Listagem 1. Comando CREATE TABLE

 CREATE TABLE "my_keyspace"."contacts" (
       contact_id uuid,
       first_name text,
       last_name text,
       phone_number text,
       PRIMARY KEY(contact_id)
  );

O comando informa que seja criada uma tabela chamada contacts dentro do keyspace my_keyspace que foi criado anteriormente. Definimos também quatro colunas para essa tabela, contact_id, que é a chave-primária e é do tipo uuid (Universally Unique Identifier), assegura que os valores dessa coluna serão únicos. As outras colunas são first_name, last_name e phone_number, todas do tipo text. Nesse caso, como a chave-primária é composta por apenas uma coluna, ela também é a partition-key dessa tabela, o hash da partition-key é o que define em que partição da tabela as linhas serão armazenadas. No caso de uma chave-primária composta, apenas a primeira coluna da chave vai ser utilizada como partition-key, as outras colunas são chamadas de clustering columns.

Outra observação importante é que uma vez definida a composição da chave-primária, ela não pode ser mais alterada, isso se deve ao fato de todo o particionamento ser definido por meio da chave. Criada a tabela, o próximo passo é inserir novas linhas nela, o que pode ser feito utilizando o comando INSERT, conforme mostra a Listagem 2.

Listagem 2. Comando INSERT

  INSERT INTO 
  "my_keyspace"."contacts" (contact_id, first_name, last_name, phone_number)
  VALUES (uuid(), 'John', 'Doe', '559912341234');
  INSERT INTO 
  "my_keyspace"."contacts" (contact_id, first_name, last_name)
  VALUES (uuid(), 'Mary', 'Jane');
  INSERT INTO 
  "my_keyspace"."contacts" (contact_id, first_name, phone_number)
  VALUES (uuid(), 'Will', '441239876543');

Executando os comandos, inserimos três novas linhas na tabela contacts. Propositalmente, nem todas as linhas são inseridas com todas as colunas preenchidas, no Cassandra, as únicas colunas que requerem um valor preenchido são as que compõem uma chave-primária. Utilizamos também a função uuid() para gerar um identificador único para a linha que será inserida. Por fim, vamos exibir o conteúdo da tabela utilizando o comando SELECT, conforme Listagem 3.

Listagem 3. Comando SELECT

  SELECT * FROM "my_keyspace"."contacts";
   
   contact_id                           | first_name | last_name | phone_number
  --------------------------------------+------------+-----------+--------------
   910ac22b-538e-42d2-a5cc-7163cdd5a0c3 |       Will |      null | 441239876543
   e7beb210-b9b6-4b08-9afb-950d5918ba2e |       John |       Doe | 559912341234
   7ef9a9af-1332-46bb-abb4-2deb9b335588 |       Mary |      Jane |         null

Como esperado, podemos ver os três registros que foram inseridos anteriormente. Um ponto importante é que não foi colocado nenhum filtro na consulta, nesse caso, o Cassandra sempre retornará por padrão as primeiras 1000 linhas da tabela. Ao adicionar filtros com uma cláusula WHERE, é necessário sempre incluir uma chave no filtro, isso também vale para os comandos UPDATE e DELETE.

Até então, salvo algumas particularidades, tudo é muito parecido com o que existe na linguagem SQL, o que ajuda muito a quem vem do mundo dos bancos relacionais. Entretanto, podemos ver as maiores diferenças quando o assunto é a modelagem de dados.

Supondo que seja necessário alterar a tabela contacts para permitir que seja gravado mais de um telefone por contato. O caminho natural em um banco de dados relacional seria aplicar a primeira forma normal criando uma segunda tabela em que seriam armazenados os telefones relacionados ao contact_id da tabela original. Porém, isso não é encorajado ao trabalharmos com o Cassandra, realizar um JOIN entre tabelas, embora possível, é custoso pelo fato da possibilidade de os dados estarem distribuídos entre vários nós diferentes do cluster. Em sistemas com essas características, é importante fazer o menor número de operações de I/O possível para garantir uma alta performance. Nesse cenário, é recomendado o uso de uma coluna do tipo Collection. Uma coleção pode ser uma lista de valores ou ainda um outro conjunto de chaves e valores. Podemos exemplificar isso criando um novo campo phone_numbers sendo uma lista de valores do tipo texto com os comandos apresentados na Listagem 4.

Listagem 4. Criação de uma coluna do tipo coleção

  ALTER TABLE my_keyspace.contacts ADD phone_numbers LIST<text>;
   
  ALTER TABLE my_keyspace.contacts DROP phone_number;
   
  UPDATE my_keyspace.contacts 
  SET phone_numbers = [ '441239876543', '441239876543' ]
  WHERE contact_id = 910ac22b-538e-42d2-a5cc-7163cdd5a0c3;
   
  SELECT phone_numbers FROM "my_keyspace"."contacts" 
  WHERE contact_id = 910ac22b-538e-42d2-a5cc-7163cdd5a0c3;
   
  phone_numbers
  ----------------------------------
   ['441239876543', '441239876543']

Nesse caso, foi criada a nova coluna utilizando o tipo LIST<text>, com isso, estamos informando uma coluna que é uma lista ordenada de valores do tipo texto. Outros tipos de coleção disponíveis são SET (lista não ordenada) e MAP (lista de chave-valor). Não é possível alterar a coluna phone_number, que é do tipo texto, para um tipo LIST<text>. O Cassandra não permite esse tipo de operação, então o comando seguinte exclui essa coluna da tabela.

A seguir, é atualizado um dos registros agora com uma lista de telefones e, por fim, um comando SELECT para exibir o resultado. No caso da Listagem 4, foi atribuída uma lista fixa de números de telefone na coleção, mas há diversas outras maneiras de manipular os dados, conforme exemplifica a Listagem 5.

Listagem 5. Operações com coleções

  UPDATE my_keyspace.contacts 
  SET phone_numbers = phone_numbers + [ '5511987654321' ]
  WHERE contact_id = 910ac22b-538e-42d2-a5cc-7163cdd5a0c3;
   
  UPDATE my_keyspace.contacts 
  SET phone_numbers = phone_numbers - [ '5511987654321' ]
  WHERE contact_id = 910ac22b-538e-42d2-a5cc-7163cdd5a0c3;
   
  UPDATE my_keyspace.contacts 
  SET phone_numbers[0] =  '5511987654321' 
  WHERE contact_id = 910ac22b-538e-42d2-a5cc-7163cdd5a0c3;
   
  UPDATE my_keyspace.contacts 
  SET phone_numbers = []
  WHERE contact_id = 910ac22b-538e-42d2-a5cc-7163cdd5a0c3;

No primeiro caso, estamos adicionando um valor no final da lista (igualmente, fazendo a declaração invertida como SET phone_numbers = [ '5511987654321' ] + phone_numbers, irá inserir os valores no início da lista). No segundo exemplo, estamos removendo o valor 5511987654321 da lista independentemente da posição que ele esteja. Podemos também especificar um valor para uma posição específica da lista como no terceiro exemplo, nesse caso, o valor anterior será substituído. Por fim, para limpar todos os valores da lista, basta atribuir à coluna um valor como uma lista vazia como demonstrado no quarto comando.

Uma coleção pode guardar até 64.000 elementos dentro dela, porém, é importante salientar que elas são projetadas para guardar pequenos conjuntos de dados, coleções muito extensas e com elementos muito complexos impactam a performance de leitura dos dados. Em um cenário desse tipo é mais indicado criar uma tabela à parte.

Outro recurso interessante no Cassandra é a criação de tipos customizados. Tipos customizados também são tratados como coleções e são úteis quando é necessário definir uma estrutura com campos pré-definidos. Digamos que é necessário criar um tipo para guardar o endereço de um contato, em que serão guardados o logradouro, a cidade, o estado e o CEP. Podemos fazer isso utilizando o comando CREATE TYPE como exemplificado na Listagem 6.

Listagem 6. Criação de um tipo customizado

CREATE TYPE my_keyspace.address (
    street text,
    city text,
    state text,
    zip_code int);

Ao definir uma coluna com esse tipo, é garantido que o campo não terá nenhuma chave a mais do que as que estão descritas (street, city, state e zip_code), contudo, não é obrigatório preencher todos os valores.

Agora vamos adicionar uma nova coluna addresses na tabela contacts que será uma coleção do tipo MAP<text, address>, com isso, teremos uma coluna podendo conter vários endereços indexados por um nome (Listagem 7).

Listagem 7. Adicionando o novo tipo address à tabela contacts

  ALTER TABLE my_keyspace.contacts
  ADD addresses MAP<text, FROZEN<address>>;
   
  UPDATE my_keyspace.contacts 
  SET addresses = addresses + {'home': 
       { street: 'Av. Paulista, 1', city: 'Sao Paulo', state: 'SP', zip_code: 1311000} }
  WHERE contact_id = 910ac22b-538e-42d2-a5cc-7163cdd5a0c3;
   
  SELECT phone_numbers, addresses FROM "my_keyspace"."contacts" 
  WHERE contact_id = 910ac22b-538e-42d2-a5cc-7163cdd5a0c3;
   
  phone_numbers                     | addresses
  ----------------------------------+-----------------------------------------
  ['441239876543', '441239876543']      | {'home': {street: 'Av. Paulista, 1',
  city: 'Sao Paulo', state: 'SP', 
  zip_code: 1311000}}

Podemos ver que para adicionar a nova coluna foi necessário definir o tipo address como FROZEN, sem isso, o Cassandra não vai permitir que a coluna seja criada. Isso se deve ao fato do Cassandra não suportar por completo coleções aninhadas (tipos customizados são tratados como coleções) devido à forma como os dados da coluna são serializados. Ao definir uma coleção como FROZEN não é possível alterar atributos individuais dela (como alterar somente o valor do atributo street da coleção address de uma linha já existente da tabela), é necessário substituir a coleção por completo. Após criar a nova coluna, inserimos um novo endereço com a chave ‘home’ para um dos contatos. Por fim, podemos ver o resultado com o comando SELECT.

Como explicado anteriormente, o Cassandra não permite fazer consultas com filtros sem que haja uma chave entre as cláusulas. Se tentarmos fazer uma consulta para trazer todos os contatos com o nome ‘Mary’, o Cassandra retornará o erro apresentado na Listagem 8.

Listagem 8. SELECT sem chave na cláusula WHERE

  SELECT first_name, last_name FROM my_keyspace.contacts
  WHERE first_name = 'Mary';
 

  InvalidRequest: code=2200 [Invalid query] message="Cannot execute this query as it
  might involve data filtering and thus may have unpredictable performance. 
  If you want to execute this query despite the performance unpredictability, 
  use ALLOW FILTERING"

Ele indica que se for executada uma consulta desse tipo, a performance é imprevisível. Isso acontece pois se essa tabela tivesse bilhões de linhas, possivelmente vários nós do cluster precisariam ser consultados para conseguir trazer o resultado desejado, seria basicamente o equivalente a um full table scan em um banco relacional, mas em uma escala muito maior. Podemos forçar a execução da consulta adicionando a instrução ALLOW FILTERING no final do comando como explicado na mensagem, mas isso não é recomendado a não ser que a tabela seja muito pequena.

Contudo, há uma forma melhor de resolver isso criando um índice secundário. Assim como em um banco relacional, no Cassandra é possível criar índices para colunas que não fazem parte da chave primária. Para isso, é utilizado o comando CREATE INDEX apresentado na Listagem 9.

Listagem 9. Criação de um índice secundário

  CREATE INDEX ON my_keyspace.contacts ( first_name );
                 
  SELECT first_name, last_name FROM my_keyspace.contacts
  WHERE first_name = 'Mary';
   
  first_name  | last_name
  ------------+-----------
         Mary |      Jane

Com o índice criado, os dados são retornados normalmente. No entanto, o uso de índices secundários não é eficiente quando:

  • A coluna tem uma cardinalidade muito alta ou muito baixa (seus valores variam bastaste ou muito pouco). Nesses casos a consulta pode acabar envolvendo muitos nós do cluster;
  • A coluna tem valores frequentemente alterados ou apagados. Isso torna a manutenção do índice onerosa, já que ele também precisa passar por constantes alterações e também ser replicado entre as máquinas.

TL (Time to Live)

Um recurso interessante e muito útil do Cassandra é a possibilidade de atribuir a um valor de uma coluna um tempo de vida, um recurso chamado TTL (abreviação de Time to Live). Passado esse tempo, o valor será automaticamente removido da tabela. O tempo é descrito em segundos e é definido utilizando o comando USING TTL como no exemplo da Listagem 10.

Listagem 10. Definição do tempo de vida de um valor

  INSERT INTO my_keyspace.contacts (contact_id, first_name, last_name)
  VALUES (uuid(), 'Temporary', 'Temporary') USING TTL 600;
   
  SELECT ttl(first_name), ttl(last_name) from my_keyspace.contacts 
   WHERE contact_id = deec4e61-1aac-442c-bd4e-d5038f900710;
   
  ttl(first_name)  | ttl(last_name)
  -----------------+----------------
               430 |            430

No exemplo, foi definido um TTL de 600 segundos (ou 10 minutos). Também é possível consultar o tempo de vida restante utilizando a função ttl(). Podemos ver que restam 430 segundos nesse caso para que os valores sejam apagados da tabela. A função ttl() irá retornar nulo caso não haja um tempo de vida definido para o valor. É possível definir tempo de vida no comando UPDATE, a diferença é que o comando USINT TTL deve estar antes da palavra-chave SET.

Níveis de consistência

Outra característica muito importante do Cassandra é a possibilidade de definir o nível de consistência para as operações de leitura e escrita no banco. Como descrito no início do artigo, os dados ficam distribuídos entre os nós do cluster por meio das partições que por sua vez podem ter réplicas de acordo com o fator de replicação definido na criação do keyspace.

No momento em que um dado é escrito, é possível definir um nível de consistência que irá dizer quantos nós precisarão ser acionados para gravar os dados. Quanto menor o nível de consistência, mais rápida e menos onerosa é a operação, porém nem todas as réplicas da partição onde foi inserido o novo dado terão a última versão do mesmo. Em contrapartida, quanto mais alto o nível de consistência definido, mais lenta e onerosa é a operação, pois ela só é considerada como concluída quando a quantidade de réplicas necessárias for atualizada. Os possíveis níveis de consistência que podem ser definidos estão descritos na Tabela 1. Da mesma forma, é possível definir níveis de consistência de leitura (Tabela 2).

Nível

Descrição

ANY

Garante que o dado seja escrito em pelo menos uma réplica antes de retornar a resposta para o cliente. Hints (BOX 1) são consideradas como uma escrita bem-sucedida.

ONE, TWO, THREE

Garante que o dado seja escrito em pelo menos um, dois ou três nós antes de retornar uma resposta para o cliente.

LOCAL_ONE

Similar ao ONE, mas com a premissa de que o dado seja escrito em pelo menos uma réplica no mesmo data center.

QUORUM

Garante que o valor foi escrito com sucesso na maioria das réplicas (calculado por: (fator de replicação / 2) + 1).

LOCAL_QUORUM

Similar ao QUORUM, mas com a premissa de que todas as réplicas sejam do mesmo data center.

EACH_QUORUM

Também similar ao QUORUM, mas garante que o valor seja escrito na maioria das réplicas em todos os data centers.

ALL

Garante que o valor seja escrito em um número de réplicas igual ao especificado no fator de replicação. A operação é considerada como falha caso não seja possível escrever em uma ou mais réplicas.

Tabela 1. Níveis de consistência de escrita

BOX 1. HINT
Uma hint é gerada dentro do Cassandra quando um nó falha em responder ao tentar executar uma operação de escrita nele, funcionando como uma espécie de lembrete. As hints são gravadas em uma tabela chamada system.hints e só são geradas caso o nível de consistência exigido no momento da escrita já foi atingido. Eventualmente, quando o nó volta a responder, o servidor que armazenou a hint vai tentar escrever novamente a informação e remover a hint caso bem-sucedido.

Nível

Descrição

ONE, TWO, THREE

Retorna o valor mais atualizado encontrado em um, dois ou três nós (de acordo com o valor especificado). Após retornar a informação para o cliente, um processo é criado em background para atualizar um valor que esteja desatualizado em uma ou mais réplicas, se necessário.

LOCAL_ONE

Similar ao ONE, mas com a premissa de que o dado seja lido em um nó no mesmo data center.

QUORUM

Consulta todos os nós até que a maioria das réplicas tenha respondido (fator de replicação / 2) + 1) retornando a versão mais atualizada. Após retornar a informação para o cliente, um processo é criado em background para atualizar um valor que esteja desatualizado em uma ou mais réplicas, se necessário.

LOCAL_QUORUM

Similar ao QUORUM, mas com a premissa de que os nós que responderem sejam todos do mesmo data center.

EACH_QUORUM

Também similar ao QUORUM, mas com a premissa de haja a resposta da maioria dos nós em todos os data centers.

ALL

Consulta todos os nós e aguarda a resposta de todos eles retornando a versão mais atualizada. Após retornar a informação para o cliente, um processo é criado em background para atualizar um valor que esteja desatualizado em uma ou mais réplicas, se necessário.

Tabela 2. Níveis de consistência de leitura

O nível de consistência é definido por conexão. Podemos consultar ou definir o nível de consistência utilizando o comando CONSISTENCY conforme apresentado na Listagem 11.

Listagem 11. Definição de consulta do nível de consistência de leitura e escrita

CONSISTENCY;
  Current consistency level is ONE.
  CONSISTENCY LOCAL_ONE;
  Consistency level set to LOCAL_ONE.

Entender o nível de consistência exigido para cada caso de uso da aplicação é importante para não onerar demais o banco sempre exigindo altos níveis de consistência. Onde não é estritamente necessário sempre ter o dado mais atualizado, é recomendado utilizar níveis de consistência mais baixos.

O Cassandra é um ótimo banco de dados para começar a entender soluções NoSQL para quem sempre teve contato com a linguagem SQL e bancos relacionais, tornando a curva de aprendizado mais suave em comparação ao MongoDB, por exemplo. No entanto, é importante se atentar às suas particularidades da modelagem de dados, já que empregar a normalização de tabelas pode trazer um grande impacto negativo na performance do banco.

Ele é uma ótima solução de alta disponibilidade e escalabilidade quando é preciso trabalhar com sistemas que requerem volumes de leitura e escrita extremamente alto e em que sempre há a chave-primária em mãos. É ótimo também para soluções em nuvem graças ao formato de sua arquitetura derivada do Dynamo, permitindo uma alta elasticidade, sendo assim, pode-se adequar a complexidade da infraestrutura de acordo com a demanda de leitura/escrita necessária.

Por outro lado, o Cassandra não é interessante para ser usado em cenários com baixa demanda de leitura e escrita (a não ser que seja esperado que essa demanda cresça rapidamente com o tempo). Também não é o melhor candidato para ser utilizado como banco de dados analítico, por ter muitas limitações quando não são feitas consultas utilizando a chave-primária, existem outras soluções que podem desempenhar melhor esse papel, como por exemplo, o HBase.