Este documento compara o Apache Cassandra e Conceitos e práticas do Spanner. Ele pressupõe que você esteja familiarizado com Cassandra e quiser migrar aplicativos existentes ou projetar novos aplicativos enquanto usa o Spanner como 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 Cassandra to Spanner Proxy Adapter. Com essa ferramenta de código aberto, é possível migrar cargas de trabalho do Cassandra ou do DataStax Enterprise (DSE) para o Spanner sem quaisquer mudanças na lógica do aplicativo.
Principais conceitos
Esta seção compara os principais conceitos do Cassandra e do Spanner.
Terminologia
Cassandra | Spanner |
---|---|
Cluster |
Instância Um cluster do Cassandra é equivalente a um Spanner instance: um conjunto de servidores e os recursos de armazenamento. Como o Spanner é um serviço gerenciado, você não precisa configurar o hardware ou o software subjacente. Você você só precisa especificar a quantidade de nós que quer reservar para sua instância ou o escalonamento automático para escalona a instância automaticamente. Uma instância atua como um contêiner bancos de dados e topologia de replicação de dados (regional, birregional ou multirregional) é escolhido 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 papéis). Ao contrário de um keyspace, não é preciso 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ção |
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 partições 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 é fragmentado por intervalo, ou seja, as linhas que são contíguos no espaço da chave primária também são contíguos no armazenamento (exceto limites de divisão). O Spanner cuida da divisão e da mesclagem 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. 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 e fragmentados por intervalos. O Spanner tem 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 veiculação e e lidam com 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 em Uso da CPU e do disco. Os fragmentos são atribuídos a nós de computação para veiculação. Dados são armazenados fisicamente no Colossus, o sistema de arquivos distribuído do Google, separado do os nós de computação. Os drivers do cliente se conectam aos servidores de front-end do Spanner, que executam o roteamento de solicitações e o balanceamento de carga. Para saber mais, consulte a Ciclo de vida das leituras e dos recursos do Spanner escreve (em inglês).
Em alto nível, as duas arquiteturas são escalonadas à medida que os recursos são adicionados do 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 outro lado da o particionamento baseado em intervalo é aquele em que as cargas de trabalho gravam em uma extremidade do espaço da chave (por exemplo, tabelas com chave de data/hora atual) podem enfrentar uso excessivo do ponto de acesso sem e considerações sobre o design do esquema. Para mais informações sobre técnicas uso excessivo do ponto de acesso, consulte as Práticas recomendadas de criação 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 do quórum, a maioria do nó de réplica deve responder ao nó 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 como se fosse um banco de dados de máquina única. Uma gravação é são sempre gravados na maioria das réplicas antes de serem confirmados pelo usuário. Todas as leituras subsequentes refletem os dados recém-gravados. Os aplicativos podem optar por ler um snapshot do banco de dados de um momento no passado, o que pode ter benefícios de desempenho em comparação a leituras fortes. Para mais informações sobre a 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 de coluna 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. Podemos pensar que o Spanner tem apenas chaves de classificação, e partições são 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 Cassandra e Spanner está nos nomes dos tipos e a 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". Para o 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á partição separada no Spanner. Os dados são armazenados classificados por todo o grupo de chave primária.
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 para saber mais 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 assinado de 64 bits)O Spanner aceita 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 (escala 9 de precisão 38).
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,
string colunas precisam especificar o comprimento máximo (não há impacto sobre
storage; isso é 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
duração de nanossegundos.
|
|
time |
int64 O Spanner não oferece suporte a um tipo de dados de tempo no dia dedicado. 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 tipados.
|
|
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 de uso básicos
Os exemplos de código a seguir 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, criar um objeto cliente vinculado a um banco de dados específico e emitir 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
É possível fazer leituras no Spanner usando uma API estilo chave-valor e uma API de consulta. Como usuário do Cassandra, talvez você considere a API de consulta mais familiar. Um
a principal diferença na API de consulta é que o Spanner exige
argumentos nomeados, ao contrário 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. Spanner.
oferece suporte a DML e a uma API de mutação de estilo de 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
Nesta seção, você encontra informações sobre como usar os 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 é
usada para manipular a semântica "last-writer-wins" da 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 oferece um conjunto de dados
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
Criar esquema com uma coluna explícita do carimbo de data/hora da atualização.
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 à 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. Em
Spanner, é possível usar UPDATE
condicional
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 é compatível com 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 esquema com uma coluna explícita de carimbo de data/hora da atualização
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
map
que contêm uma pequena quantidade de dados no Spanner, você
pode usar o JSON
ou o PROTO
que permitem armazenar dados estruturados e semiestruturados, respectivamente.
As atualizações nessas colunas exigem a regravação de todo o valor da coluna. Se você
tem um caso de uso em que uma grande quantidade de dados é armazenada em um map
do Cassandra; e
apenas uma pequena parte do map
precisa ser atualizada, usando
INTERLEAVED
tabelas podem 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 o linha de usuário e pode ser recuperada e atualizada de forma eficiente junto com a linha de usuário. É possível usar o método read-write APIs no Spanner para interagir com tabelas intercaladas. Para mais informações sobre intercalação, consulte Crie tabelas mãe e filha.
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 teste de unidade. O Spanner oferece um ambiente semelhante para desenvolvimento local usando o emulador do Spanner. O emulador oferece um alto de fidelidade para desenvolvimento interativo e testes de unidade. Para mais para mais informações, consulte Emular o Spanner localmente.
Linha de comando
O equivalente no Spanner ao nodetool
do Cassandra é
Google Cloud CLI. É possível realizar operações de plano de controle e de dados
usando gcloud spanner
. Para mais informações, consulte a
Guia de referência do Spanner da CLI 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
em 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.