Este documento compara os conceitos e práticas do Apache Cassandra e do Spanner. Pressupo que você já conhece o Cassandra e quer migrar aplicativos existentes ou projetar novos usando o Spanner como seu banco de dados.
O Cassandra e o Spanner são bancos de dados distribuídos em grande escala criados para aplicativos que exigem alta escalabilidade e baixa latência. Embora os dois bancos de dados possam oferecer suporte a cargas de trabalho NoSQL exigentes, o Spanner oferece recursos avançados para modelagem de dados, consultas e operações transacionais. Para mais informações sobre como o Spanner atende aos critérios de banco de dados NoSQL, consulte Spanner para cargas de trabalho não relacionais.
Migrar do Cassandra para o Spanner
Para migrar do Cassandra para o Spanner, use o Adaptador de proxy do Cassandra para o Spanner. Essa ferramenta de código aberto permite migrar cargas de trabalho do Cassandra para o Spanner sem mudar a lógica do aplicativo.
Principais conceitos
Esta seção compara os principais conceitos do Cassandra e do Spanner.
Terminologia
Cassandra | Spanner |
---|---|
Cluster |
Instância O cluster do Cassandra é equivalente a uma instância do Spanner, uma coleção de servidores e recursos de armazenamento. Como o Spanner é um serviço gerenciado, você não precisa configurar o hardware ou o software subjacente. Você só precisa especificar a quantidade de nós que quer reservar para a instância ou escolher escalonamento automático para escalonar a instância automaticamente. Uma instância funciona como um contêiner para bancos de dados, e a topologia de replicação de dados (regional, birregional ou multirregional) é escolhida no nível da instância. |
Keyspace (em inglês) |
Banco de dados Um keyspace do Cassandra é equivalente a um banco de dados Spanner, que é uma coleção de tabelas e outros elementos de esquema (por exemplo, índices e funções). Ao contrário de um keyspace, você não precisa configurar o fator de replicação. O Spanner replica automaticamente seus dados para a região designada na instância. |
Tabela |
Tabela No Cassandra e no Spanner, as tabelas são uma coleção de linhas identificadas por uma chave primária especificada no esquema da tabela. |
Partições |
Divisão Tanto o Cassandra quanto o Spanner são dimensionados por fragmentação de dados. No Cassandra, cada fragmento é chamado de partição, enquanto no Spanner, cada fragmento é chamado de divisão. O Cassandra usa a partição de hash, o que significa que cada linha é atribuída de forma independente a um nó de armazenamento com base em um hash da chave primária. O Spanner é dividido em intervalos, o que significa que as linhas que são contíguas no espaço da chave primária também são contíguas no armazenamento (exceto nos limites de divisão). O Spanner divide e mescla os dados com base na carga e no armazenamento, e isso é transparente para o aplicativo. A implicação principal é que, ao contrário do Cassandra, a verificação de intervalo em um prefixo da chave primária é uma operação eficiente no Spanner. |
Row |
Linha No Cassandra e no Spanner, uma linha é uma coleção de colunas identificada exclusivamente por uma chave primária. Assim como o Cassandra, o Spanner oferece suporte a chaves primárias compostas. Ao contrário do Cassandra, o Spanner não faz distinção entre chave de partição e chave de classificação, porque os dados são divididos em intervalos. O Spanner pode ser considerado como tendo apenas chaves de classificação, com o particionamento gerenciado em segundo plano. |
Coluna |
Coluna No Cassandra e no Spanner, uma coluna é um conjunto de valores de dados que têm o mesmo tipo. Há um valor para cada linha de uma tabela. Para mais informações sobre como comparar os tipos de coluna do Cassandra com o Spanner, consulte Tipos de dados. |
Arquitetura
Um cluster do Cassandra consiste em um conjunto de servidores e armazenamentos colocalizados com esses servidores. Uma função hash mapeia linhas de um espaço de chave de partição para um nó virtual (vnode). Um conjunto de vnodes é atribuído aleatoriamente a cada servidor para veicular uma parte do keyspace do cluster. O armazenamento dos vnodes é anexado localmente ao nó de exibição. Os drivers de cliente se conectam diretamente aos nós de serviço e processam o balanceamento de carga e o roteamento de consultas.
Uma instância do Spanner consiste em um conjunto de servidores em uma topologia de replicação. O Spanner fragmenta dinamicamente cada tabela em intervalos de linhas com base no uso da CPU e do disco. Os fragmentos são atribuídos a nós de computação para veiculação. Os dados são armazenados fisicamente no Colossus, o sistema de arquivos distribuídos do Google, separado dos nós de computação. Os drivers do cliente se conectam aos servidores front-end do Spanner, que executam o roteamento de solicitações e o balanceamento de carga. Para saber mais, consulte o artigo técnico Vida útil das leituras e gravações do Spanner.
Em um nível alto, as duas arquiteturas são dimensionadas à medida que os recursos são adicionados ao cluster. A separação de computação e armazenamento do Spanner permite um rebalanceamento mais rápido da carga entre nós de computação em resposta a mudanças na carga de trabalho. Ao contrário do Cassandra, os movimentos de fragmento não envolvem a movimentação de dados, já que eles permanecem no Colossus. Além disso, o particionamento baseado em intervalo do Spanner pode ser mais natural para aplicativos que esperam que os dados sejam classificados por chave de partição. O lado negativo do particionamento baseado em intervalo é que as cargas de trabalho que gravam em uma extremidade do espaço da chave (por exemplo, tabelas codificadas por carimbo de data/hora atual) podem enfrentar problemas de ponto de acesso sem consideração adicional de design do esquema. Para mais informações sobre técnicas para superar pontos de acesso, consulte Práticas recomendadas de design de esquema.
Consistência
No Cassandra, é necessário especificar um nível de consistência para cada operação. Se você usar o nível de consistência de quórum, a maioria do nó de réplica precisa responder ao nó do coordenador para que a operação seja considerada bem-sucedida. Se você usar um nível de consistência de um, o Cassandra precisará de um único nó de réplica para responder e a operação ser considerada bem-sucedida.
O Spanner oferece consistência forte. A API Spanner não expõe réplicas ao cliente. Os clientes do Spanner interagem com ele como se fosse um banco de dados de uma única máquina. Uma gravação é sempre gravada na maioria das réplicas antes de ser confirmada para o usuário. Todas as leituras subsequentes refletem os dados recém-gravados. Os aplicativos podem escolher ler um snapshot do banco de dados em um momento anterior, o que pode ter benefícios de desempenho em relação a leituras fortes. Para mais informações sobre as propriedades de consistência do Spanner, consulte a Visão geral das transações.
O Spanner foi criado para oferecer a consistência e a disponibilidade necessárias em aplicativos de grande escala. O Spanner oferece consistência forte em grande escala e com alto desempenho. Para casos de uso que exigem isso, o Spanner oferece suporte a leituras de snapshots que relaxam os requisitos de atualização.
Modelagem de dados
Esta seção compara os modelos de dados do Cassandra e do Spanner.
Declaração de tabela
A sintaxe de declaração de tabela é bastante semelhante no Cassandra e no Spanner. Você especifica o nome da tabela, os nomes e tipos das colunas e a chave primária que identifica exclusivamente uma linha. A principal diferença é que o Cassandra é particionado por hash e faz uma distinção entre a chave de partição e a chave de classificação, enquanto o Spanner é particionado por intervalo. O Spanner pode ser considerado como tendo apenas chaves de classificação, com partições mantidas automaticamente em segundo plano. Assim como o Cassandra, o Spanner oferece suporte a chaves primárias compostas.
Parte única da chave primária
A diferença entre o Cassandra e o Spanner está nos nomes de tipo e na localização da cláusula de chave primária.
Cassandra | Spanner |
---|---|
CREATE TABLE users ( user_id bigint, first_name text, last_name text, PRIMARY KEY (user_id) ) |
CREATE TABLE users ( user_id int64, first_name string(max), last_name string(max), ) PRIMARY KEY (user_id) |
Várias partes da chave primária
No Cassandra, a primeira parte da chave primária é a "chave de partição", e as partes da chave primária subsequentes são "chaves de classificação". No Spanner, não há uma chave de partição separada. Os dados são armazenados classificados pela chave primária composta inteira.
Cassandra | Spanner |
---|---|
CREATE TABLE user_items ( user_id bigint, item_id bigint, first_name text, last_name text, PRIMARY KEY (user_id, item_id) ) |
CREATE TABLE user_items ( user_id int64, item_id int64, first_name string(max), last_name string(max), ) PRIMARY KEY (user_id, item_id) |
Chave de partição composta
No Cassandra, as chaves de partição podem ser compostas. Não há uma chave de partição separada no Spanner. Os dados são armazenados classificados pela chave primária composta inteira.
Cassandra | Spanner |
---|---|
CREATE TABLE user_category_items ( user_id bigint, category_id bigint, item_id bigint, first_name text, last_name text, PRIMARY KEY ((user_id, category_id), item_id) ) |
CREATE TABLE user_category_items ( user_id int64, category_id int64, item_id int64, first_name string(max), last_name string(max), ) PRIMARY KEY (user_id, category_id, item_id) |
Tipos de dados
Esta seção compara os tipos de dados do Cassandra e do Spanner. Para mais informações sobre os tipos do Spanner, consulte Tipos de dados no GoogleSQL.
Cassandra | Spanner | |
---|---|---|
Tipos numéricos |
Números inteiros padrão:bigint (número inteiro assinado de 64 bits)int (número inteiro assinado de 32 bits)smallint (número inteiro assinado de 16 bits)tinyint (número inteiro assinado de 8 bits)
|
int64 (número inteiro de 64 bits)O Spanner oferece suporte a um único tipo de dados de 64 bits para números inteiros assinados. |
Ponto flutuante padrão:double (ponto flutuante IEEE-754 de 64 bits)float (ponto flutuante IEEE-754 de 32 bits) |
float64 (ponto flutuante IEEE-754 de 64 bits)float32 (ponto flutuante IEEE-754 de 32 bits)
|
|
Números de precisão variável:varint (número inteiro de precisão variável)decimal (número decimal de precisão variável)
|
Para números decimais de precisão fixa, use numeric (precisão 38 escala 9).
Caso contrário, use string com uma biblioteca de precisão de variável
de camada de aplicativo.
|
|
Tipos de string |
text varchar
|
string(max) text e varchar armazenam e validam strings UTF-8. No Spanner,
as colunas string precisam especificar o comprimento máximo. Isso não afeta o
armazenamento, é apenas para fins de validação.
|
blob |
bytes(max) Para armazenar dados binários, use o tipo de dados bytes .
|
|
Tipos de data e hora | date |
date |
duration |
int64 O Spanner não oferece suporte a um tipo de dados de duração dedicado. Use int64 para armazenar
a duração em nanossegundos.
|
|
time |
int64 O Spanner não oferece suporte a um tipo de dados dedicado de horário do dia. Use int64 para
armazenar o deslocamento de nanossegundos em um dia.
|
|
timestamp |
timestamp |
|
Tipos de contêiner | Tipos definidos pelo usuário | json ou proto |
list |
array Use array para armazenar uma lista de objetos digitados.
|
|
map |
json ou proto O Spanner não oferece suporte a um tipo de mapa dedicado. Use colunas json ou proto para representar mapas. Para mais informações, consulte Armazenar mapas grandes como tabelas intercaladas.
|
|
set |
array O Spanner não oferece suporte a um tipo de conjunto dedicado. Use colunas array para representar
set , com o aplicativo gerenciando a exclusividade do conjunto. Para mais informações, consulte Armazenar mapas grandes como tabelas intercaladas,
que também pode ser usado para armazenar conjuntos grandes.
|
Padrões básicos de uso
Os exemplos de código abaixo mostram a diferença entre o código do cliente do Cassandra e do Spanner em Go. Para mais informações, consulte Bibliotecas de cliente do Spanner.
Inicialização do cliente
Em clientes do Cassandra, você cria um objeto de cluster que representa o cluster subjacente do Cassandra, instancia um objeto de sessão que abstrai uma conexão com o cluster e emite consultas na sessão. No Spanner, você cria um objeto cliente vinculado a um banco de dados específico e emite solicitações de banco de dados no objeto cliente.
Exemplo do Cassandra
Go
import "github.com/gocql/gocql" ... cluster := gocql.NewCluster("<address>") cluster.Keyspace = "<keyspace>" session, err := cluster.CreateSession() if err != nil { return err } defer session.Close() // session.Query(...)
Exemplo do Spanner
Go
import "cloud.google.com/go/spanner" ... client, err := spanner.NewClient(ctx, fmt.Sprintf("projects/%s/instances/%s/databases/%s", project, instance, database)) defer client.Close() // client.Apply(...)
Ler dados
As leituras no Spanner podem ser realizadas usando uma API de estilo chave-valor e uma API de consulta. Como usuário do Cassandra, você pode achar a API de consulta mais familiar. Uma
diferença importante na API de consulta é que o Spanner exige
argumentos nomeados (diferente dos argumentos posicionais ?
no Cassandra). O nome de um
argumento em uma consulta do Spanner precisa ter o prefixo @
.
Exemplo do Cassandra
Go
stmt := `SELECT user_id, first_name, last_name FROM users WHERE user_id = ?` var ( userID int firstName string lastName string ) err := session.Query(stmt, 1).Scan(&userID, &firstName, &lastName)
Exemplo do Spanner
Go
stmt := spanner.Statement{ SQL: `SELECT user_id, first_name, last_name FROM users WHERE user_id = @user_id`, Params: map[string]any{"user_id": 1}, } var ( userID int64 firstName string lastName string ) err := client.Single().Query(ctx, stmt).Do(func(row *spanner.Row) error { return row.Columns(&userID, &firstName, &lastName) })
Insira os dados
Um INSERT
do Cassandra é equivalente a um INSERT OR UPDATE
do Spanner.
É necessário especificar a chave primária completa para uma inserção. O Spanner
é compatível com a DML e uma API de mutação de estilo chave-valor. A API de mutação de estilo chave-valor é recomendada para gravações triviais devido à latência mais baixa.
A API Spanner DML tem mais recursos, já que oferece suporte à superfície SQL completa (incluindo
o uso de expressões na instrução DML).
Exemplo do Cassandra
Go
stmt := `INSERT INTO users (user_id, first_name, last_name) VALUES (?, ?, ?)` err := session.Query(stmt, 1, "John", "Doe").Exec()
Exemplo do Spanner
Go
_, err := client.Apply(ctx, []*spanner.Mutation{ spanner.InsertOrUpdateMap( "users", map[string]any{ "user_id": 1, "first_name": "John", "last_name": "Doe", } )})
Inserir dados em lote
No Cassandra, é possível inserir várias linhas usando uma instrução de lote. No Spanner, uma operação de confirmação pode conter várias mutações. O Spanner insere essas mutações no banco de dados de forma atômica.
Exemplo do Cassandra
Go
stmt := `INSERT INTO users (user_id, first_name, last_name) VALUES (?, ?, ?)` b := session.NewBatch(gocql.UnloggedBatch) b.Entries = []gocql.BatchEntry{ {Stmt: stmt, Args: []any{1, "John", "Doe"}}, {Stmt: stmt, Args: []any{2, "Mary", "Poppins"}}, } err = session.ExecuteBatch(b)
Exemplo do Spanner
Go
_, err := client.Apply(ctx, []*spanner.Mutation{ spanner.InsertOrUpdateMap( "users", map[string]any{ "user_id": 1, "first_name": "John", "last_name": "Doe" }, ), spanner.InsertOrUpdateMap( "users", map[string]any{ "user_id": 2, "first_name": "Mary", "last_name": "Poppins", }, ), })
Excluir dados
As exclusões do Cassandra exigem a especificação da chave primária das linhas a serem excluídas.
Isso é semelhante à mutação DELETE
no Spanner.
Exemplo do Cassandra
Go
stmt := `DELETE FROM users WHERE user_id = ?` err := session.Query(stmt, 1).Exec()
Exemplo do Spanner
Go
_, err := client.Apply(ctx, []*spanner.Mutation{ spanner.Delete("users", spanner.Key{1}), })
Tópicos avançados
Esta seção contém informações sobre como usar recursos mais avançados do Cassandra no Spanner.
Carimbo de data/hora de gravação
O Cassandra permite que as mutações especifiquem explicitamente um carimbo de data/hora de gravação para uma
célula específica usando a cláusula USING TIMESTAMP
. Normalmente, esse recurso é
usado para manipular a semântica de último escritor vence do Cassandra.
O Spanner não permite que os clientes especifiquem o carimbo de data/hora de cada
gravação. Cada célula é marcada internamente com o carimbo de data/hora TrueTime no momento em que o valor da célula foi confirmado. Como o Spanner fornece uma interface
consistente e estritamente serializável, a maioria dos aplicativos não precisa da
funcionalidade de USING TIMESTAMP
.
Se você usa o USING TIMESTAMP
do Cassandra para a lógica específica do aplicativo, adicione uma coluna TIMESTAMP
extra ao esquema do Spanner, que pode rastrear o tempo de modificação no nível do aplicativo. As atualizações de uma linha podem ser agrupadas em uma transação de leitura e gravação. Exemplo:
Exemplo do Cassandra
Go
stmt := `INSERT INTO users (user_id, first_name, last_name) VALUES (?, ?, ?) USING TIMESTAMP ?` err := session.Query(stmt, 1, "John", "Doe", ts).Exec()
Exemplo do Spanner
Crie um esquema com uma coluna de carimbo de data/hora de atualização explícita.
GoogleSQL
CREATE TABLE users ( user_id INT64, first_name STRING(MAX), last_name STRING(MAX), update_ts TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true), ) PRIMARY KEY (user_id)
Personalize a lógica para atualizar a linha e incluir um carimbo de data/hora.
Go
func ShouldUpdateRow(ctx context.Context, txn *spanner.ReadWriteTransaction, updateTs time.Time) (bool, error) { // Read the existing commit timestamp. row, err := txn.ReadRow(ctx, "users", spanner.Key{1}, []string{"update_ts"}) // Treat non-existent row as NULL timestamp - the row should be updated. if spanner.ErrCode(err) == codes.NotFound { return true, nil } // Propagate unexpected errors. if err != nil { return false, err } // Check if the committed timestamp is newer than the update timestamp. var committedTs *time.Time err = row.Columns(&committedTs) if err != nil { return false, err } if committedTs != nil && committedTs.Before(updateTs) { return false, nil } // Committed timestamp is older than update timestamp - the row should be updated. return true, nil }
Verifique a condição personalizada antes de atualizar a linha.
Go
_, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error { // Check if the row should be updated. ok, err := ShouldUpdateRow(ctx, txn, time.Now()) if err != nil { return err } if !ok { return nil } // Update the row. txn.BufferWrite([]*spanner.Mutation{ spanner.InsertOrUpdateMap("users", map[string]any{ "user_id": 1, "first_name": "John", "last_name": "Doe", "update_ts": spanner.CommitTimestamp, })}) return nil })
Mutações condicionais
A instrução INSERT ... IF EXISTS
no Cassandra é equivalente à instrução INSERT
no Spanner. Em ambos os casos, a inserção falha se a linha
já existir.
No Cassandra, também é possível criar instruções DML que especifiquem uma condição, e
a instrução falhará se a condição for avaliada como falsa. No
Spanner, é possível usar mutações UPDATE
condicionais em transações de leitura/gravação. Por exemplo, para atualizar uma linha somente se uma
condição específica existir:
Exemplo do Cassandra
Go
stmt := `UPDATE users SET last_name = ? WHERE user_id = ? IF first_name = ?` err := session.Query(stmt, 1, "Smith", "John").Exec()
Exemplo do Spanner
Personalize a lógica para atualizar a linha e incluir uma condição.
Go
func ShouldUpdateRow(ctx context.Context, txn *spanner.ReadWriteTransaction) (bool, error) { row, err := txn.ReadRow(ctx, "users", spanner.Key{1}, []string{"first_name"}) if err != nil { return false, err } var firstName *string err = row.Columns(&firstName) if err != nil { return false, err } if firstName != nil && firstName == "John" { return false, nil } return true, nil }
Verifique a condição personalizada antes de atualizar a linha.
Go
_, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error { ok, err := ShouldUpdateRow(ctx, txn, time.Now()) if err != nil { return err } if !ok { return nil } txn.BufferWrite([]*spanner.Mutation{ spanner.InsertOrUpdateMap("users", map[string]any{ "user_id": 1, "last_name": "Smith", "update_ts": spanner.CommitTimestamp, })}) return nil })
TTL
O Cassandra oferece suporte para a definição de um valor de time to live (TTL) no nível da linha ou da coluna. No Spanner, o TTL é configurado no nível da linha, e você designa uma coluna nomeada como o tempo de expiração da linha. Para mais informações, consulte a Visão geral do Time to live (TTL).
Exemplo do Cassandra
Go
stmt := `INSERT INTO users (user_id, first_name, last_name) VALUES (?, ?, ?) USING TTL 86400 ?` err := session.Query(stmt, 1, "John", "Doe", ts).Exec()
Exemplo do Spanner
Criar um esquema com uma coluna de carimbo de data/hora de atualização explícita
GoogleSQL
CREATE TABLE users ( user_id INT64, first_name STRING(MAX), last_name STRING(MAX), update_ts TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true), ) PRIMARY KEY (user_id), ROW DELETION POLICY (OLDER_THAN(update_ts, INTERVAL 1 DAY));
Insira linhas com um carimbo de data/hora de confirmação.
Go
_, err := client.Apply(ctx, []*spanner.Mutation{ spanner.InsertOrUpdateMap("users", map[string]any{ "user_id": 1, "first_name": "John", "last_name": "Doe", "update_ts": spanner.CommitTimestamp}), })
Armazene mapas grandes como tabelas intercaladas.
O Cassandra oferece suporte ao tipo map
para armazenar pares de chave-valor ordenados. Para armazenar
tipos map
que contêm uma pequena quantidade de dados no Spanner,
use os tipos JSON
ou PROTO
,
que permitem armazenar dados semiestruturados e estruturados, respectivamente.
As atualizações nessas colunas exigem que o valor da coluna inteira seja gravado novamente. Se você
tiver um caso de uso em que uma grande quantidade de dados for armazenada em um map
do Cassandra e
apenas uma pequena parte do map
precisar ser atualizada, o uso de
tabelas INTERLEAVED
pode ser uma boa opção. Por exemplo, para associar uma grande quantidade de dados de chave-valor a um usuário específico:
Exemplo do Cassandra
CREATE TABLE users (
user_id bigint,
attachments map<string, string>,
PRIMARY KEY (user_id)
)
Exemplo do Spanner
CREATE TABLE users (
user_id INT64,
) PRIMARY KEY (user_id);
CREATE TABLE user_attachments (
user_id INT64,
attachment_key STRING(MAX),
attachment_val STRING(MAX),
) PRIMARY KEY (user_id, attachment_key);
Nesse caso, uma linha de anexos do usuário é armazenada junto com a linha de usuário correspondente e pode ser recuperada e atualizada de maneira eficiente com a linha de usuário. É possível usar as APIs de leitura e gravação no Spanner para interagir com tabelas intercaladas. Para mais informações sobre a intercalação, consulte Criar tabelas principais e secundárias.
Experiência do desenvolvedor
Esta seção compara as ferramentas para desenvolvedores do Spanner e do Cassandra.
Desenvolvimento local
É possível executar o Cassandra localmente para desenvolvimento e testes de unidade. O Spanner oferece um ambiente semelhante para desenvolvimento local usando o emulador do Spanner. O emulador oferece um ambiente de alta fidelidade para desenvolvimento interativo e testes de unidade. Para mais informações, consulte Emulação local do Spanner.
Linha de comando
O equivalente do Spanner ao nodetool
do Cassandra é a
CLI do Google Cloud. É possível realizar operações de plano de controle e de dados
usando gcloud spanner
. Para mais informações, consulte o
guia de referência da CLI do Spanner do Google Cloud.
Se você precisar de uma interface REPL para emitir consultas ao Spanner
semelhantes a cqlsh
, use a ferramenta spanner-cli
. Para instalar e executar
spanner-cli
no Go:
go install github.com/cloudspannerecosystem/spanner-cli@latest
$(go env GOPATH)/bin/spanner-cli
Para mais informações, consulte o repositório spanner-cli do GitHub.