Padrões de Banco de Dados Multi-Inquilino com exemplos em Go

Guia completo sobre padrões de banco de dados multi-tenant

Conteúdo da página

Multi-tenancy é um padrão arquitetural fundamental para aplicações SaaS, permitindo que múltiplos clientes (inquilinos) compartilhem a mesma infraestrutura de aplicação, mantendo a isolamento de dados.

Escolher o padrão correto de banco de dados é crucial para escalabilidade, segurança e eficiência operacional.

databases-scheme

Visão Geral dos Padrões de Multi-Inquilino

Ao projetar uma aplicação multi-inquilino, você tem três padrões principais de arquitetura de banco de dados para escolher:

  1. Banco de Dados Compartilhado, Esquema Compartilhado (mais comum)
  2. Banco de Dados Compartilhado, Esquema Separado
  3. Banco de Dados Separado por Inquilino

Cada padrão tem características distintas, trade-offs e casos de uso. Vamos explorar cada um em detalhe.

Padrão 1: Banco de Dados Compartilhado, Esquema Compartilhado

Este é o padrão mais comum de multi-inquilino, onde todos os inquilinos compartilham o mesmo banco de dados e esquema, com uma coluna tenant_id usada para distinguir os dados dos inquilinos.

Arquitetura

┌─────────────────────────────────────┐
│     Single Database                 │
│  ┌───────────────────────────────┐  │
│  │  Shared Schema                │  │
│  │  - users (tenant_id, ...)     │  │
│  │  - orders (tenant_id, ...)    │  │
│  │  - products (tenant_id, ...)  │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

Exemplo de Implementação

Ao implementar padrões de multi-inquilino, compreender fundamentos do SQL é crucial. Para uma referência abrangente sobre comandos e sintaxe SQL, consulte nossa SQL Cheatsheet. Aqui está como configurar o padrão de esquema compartilhado:

-- Tabela de usuários com tenant_id
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    tenant_id INTEGER NOT NULL,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255),
    created_at TIMESTAMP DEFAULT NOW(),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);

-- Índice em tenant_id para desempenho
CREATE INDEX idx_users_tenant_id ON users(tenant_id);

-- Segurança de Linha (exemplo do PostgreSQL)
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON users
    FOR ALL
    USING (tenant_id = current_setting('app.current_tenant')::INTEGER);

Para mais recursos específicos do PostgreSQL, incluindo políticas RLS, gerenciamento de esquema e ajuste de desempenho, consulte nossa PostgreSQL Cheatsheet.

Filtragem no Nível da Aplicação

Ao trabalhar com aplicações Go, escolher o ORM certo pode impactar significativamente sua implementação de multi-inquilino. Os exemplos abaixo usam GORM, mas existem várias opções excelentes disponíveis. Para uma comparação detalhada de ORMs Go incluindo GORM, Ent, Bun e sqlc, veja nossa guia abrangente sobre ORMs Go para PostgreSQL.

// Exemplo em Go com GORM
func GetUserByEmail(db *gorm.DB, tenantID uint, email string) (*User, error) {
    var user User
    err := db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user).Error
    return &user, err
}

// Middleware para definir o contexto do inquilino
func TenantMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tenantID := extractTenantID(r) // Do subdomínio, cabeçalho ou JWT
        ctx := context.WithValue(r.Context(), "tenant_id", tenantID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Vantagens do Esquema Compartilhado

  • Custo mais baixo: Instância única do banco de dados, infraestrutura mínima
  • Operações mais simples: Um único banco de dados para backup, monitoramento e manutenção
  • Alterações de esquema simples: Migrações aplicam-se a todos os inquilinos ao mesmo tempo
  • Melhor para alto número de inquilinos: Utilização eficiente de recursos
  • Análise entre inquilinos: Fácil de agregar dados entre inquilinos

Desvantagens do Esquema Compartilhado

  • Isolamento mais fraco: Risco de vazamento de dados se as consultas esquecerem o filtro tenant_id
  • Inquilino barulhento: Carga de trabalho pesada de um inquilino pode afetar outros
  • Personalização limitada: Todos os inquilinos compartilham o mesmo esquema
  • Desafios de conformidade: Mais difícil de atender a requisitos rigorosos de isolamento de dados
  • Complexidade de backup: Não é possível restaurar facilmente dados de um único inquilino

Melhor para Esquema Compartilhado

  • Aplicações SaaS com muitos inquilinos pequenos a médios
  • Aplicações onde os inquilinos não precisam de esquemas personalizados
  • Startups sensíveis ao custo
  • Quando o número de inquilinos é alto (milhares+)

Padrão 2: Banco de Dados Compartilhado, Esquema Separado

Cada inquilino recebe seu próprio esquema dentro do mesmo banco de dados, fornecendo melhor isolamento enquanto compartilha a infraestrutura.

Arquitetura de Esquema Separado

┌─────────────────────────────────────┐
│     Single Database                 │
│  ┌──────────┐  ┌──────────┐         │
│  │ Schema A │  │ Schema B │  ...    │
│  │ (Tenant1)│  │ (Tenant2)│         │
│  └──────────┘  └──────────┘         │
└─────────────────────────────────────┘

Implementação de Esquema Separado

Os esquemas do PostgreSQL são uma funcionalidade poderosa para multi-inquilino. Para informações detalhadas sobre gerenciamento de esquema do PostgreSQL, strings de conexão e comandos de administração de banco de dados, consulte nossa PostgreSQL Cheatsheet.

-- Crie um esquema para o inquilino
CREATE SCHEMA tenant_123;

-- Defina o caminho de pesquisa para operações do inquilino
SET search_path TO tenant_123, public;

-- Crie tabelas no esquema do inquilino
CREATE TABLE tenant_123.users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255),
    created_at TIMESTAMP DEFAULT NOW()
);

Gerenciamento de Conexão da Aplicação

Gerenciar conexões de banco de dados de forma eficiente é crítico para aplicações multi-inquilino. O código de gerenciamento de conexão abaixo usa GORM, mas você pode querer explorar outras opções de ORM. Para uma comparação detalhada de ORMs Go incluindo pooling de conexão, características de desempenho e casos de uso, consulte nossa guia de comparação de ORMs Go.

// String de conexão com caminho de pesquisa do esquema
func GetTenantDB(tenantID uint) *gorm.DB {
    db := initializeDB()
    db.Exec(fmt.Sprintf("SET search_path TO tenant_%d, public", tenantID))
    return db
}

// Ou use a string de conexão do PostgreSQL
// postgresql://user:pass@host/db?search_path=tenant_123

Vantagens do Esquema Separado

  • Melhor isolamento: Separação no nível do esquema reduz o risco de vazamento de dados
  • Personalização: Cada inquilino pode ter estruturas de tabela diferentes
  • Custo moderado: Ainda é uma instância única do banco de dados
  • Backups por inquilino mais fáceis: É possível fazer backup de esquemas individuais
  • Melhor para conformidade: Mais forte do que o padrão de esquema compartilhado

Desvantagens do Esquema Separado

  • Complexidade de gerenciamento de esquema: Migrações devem ser executadas por inquilino
  • Sobrecarga de conexão: É necessário definir search_path por conexão
  • Escalabilidade limitada: O número de esquemas tem limites (PostgreSQL ~10k esquemas)
  • Consultas entre inquilinos: Mais complexas, exigem referências dinâmicas de esquema
  • Limites de recursos: Recursos do banco de dados ainda são compartilhados

Melhor para Esquema Separado

  • SaaS de média escala (dezenas a centenas de inquilinos)
  • Quando os inquilinos precisam de personalização de esquema
  • Aplicações que precisam de melhor isolamento do que o esquema compartilhado
  • Quando os requisitos de conformidade são moderados

Padrão 3: Banco de Dados Separado por Inquilino

Cada inquilino recebe sua própria instância completa de banco de dados, fornecendo o máximo de isolamento.

Arquitetura de Banco de Dados Separado

┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│  Database 1  │  │  Database 2  │  │  Database 3  │
│  (Tenant A)  │  │  (Tenant B)  │  │  (Tenant C)  │
└──────────────┘  └──────────────┘  └──────────────┘

Implementação de Banco de Dados Separado

-- Crie um banco de dados para o inquilino
CREATE DATABASE tenant_enterprise_corp;

-- Conecte-se ao banco de dados do inquilino
\c tenant_enterprise_corp

-- Crie tabelas (não é necessário tenant_id!)
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255),
    created_at TIMESTAMP DEFAULT NOW()
);

Gerenciamento Dinâmico de Conexão

// Gerenciador de pool de conexão
type TenantDBManager struct {
    pools map[uint]*gorm.DB
    mu    sync.RWMutex
}

func (m *TenantDBManager) GetDB(tenantID uint) (*gorm.DB, error) {
    m.mu.RLock()
    if db, exists := m.pools[tenantID]; exists {
        m.mu.RUnlock()
        return db, nil
    }
    m.mu.RUnlock()

    m.mu.Lock()
    defer m.mu.Unlock()

    // Verifique novamente após adquirir o bloqueio de escrita
    if db, exists := m.pools[tenantID]; exists {
        return db, nil
    }

    // Crie uma nova conexão
    db, err := gorm.Open(postgres.Open(fmt.Sprintf(
        "host=localhost user=dbuser password=dbpass dbname=tenant_%d sslmode=disable",
        tenantID,
    )), &gorm.Config{})
    
    if err != nil {
        return nil, err
    }

    m.pools[tenantID] = db
    return db, nil
}

Vantagens de Banco de Dados Separado

  • Isolamento máximo: Separação completa de dados
  • Melhor segurança: Nenhum risco de acesso a dados entre inquilinos
  • Personalização total: Cada inquilino pode ter esquemas completamente diferentes
  • Escalabilidade independente: Escalabilidade individual dos bancos de dados dos inquilinos
  • Conformidade fácil: Atende aos requisitos mais rigorosos de isolamento de dados
  • Backups por inquilino: Simples, backup/restauração independente
  • Nenhum inquilino barulhento: Cargas de trabalho de inquilinos não afetam uns aos outros

Desvantagens de Banco de Dados Separado

  • Custo mais alto: Múltiplas instâncias de banco de dados exigem mais recursos
  • Complexidade operacional: Gerenciar muitos bancos de dados (backups, monitoramento, migrações)
  • Limites de conexão: Cada instância de banco de dados tem limites de conexão
  • Análise entre inquilinos: Requer federação de dados ou ETL
  • Complexidade de migração: Deve executar migrações em todos os bancos de dados
  • Sobrecarga de recursos: Mais memória, CPU e armazenamento necessários

Melhor para Banco de Dados Separado

  • SaaS empresarial com clientes de alto valor
  • Requisitos de conformidade rigorosos (HIPAA, GDPR, SOC 2)
  • Quando os inquilinos precisam de personalização significativa
  • Número de inquilinos baixo a médio (dezenas a centenas baixas)
  • Quando os inquilinos têm modelos de dados muito diferentes

Considerações de Segurança

Independentemente do padrão escolhido, a segurança é primordial:

1. Segurança de Linha (RLS)

A RLS do PostgreSQL filtra automaticamente consultas por inquilino, fornecendo uma camada de segurança no nível do banco de dados. Esta funcionalidade é particularmente poderosa para aplicações multi-inquilino. Para mais detalhes sobre RLS do PostgreSQL, políticas de segurança e outras funcionalidades avançadas do PostgreSQL, veja nossa PostgreSQL Cheatsheet.

-- Ative RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- Política para isolar por inquilino
CREATE POLICY tenant_isolation ON orders
    FOR ALL
    USING (tenant_id = current_setting('app.current_tenant')::INTEGER);

-- A aplicação define o contexto do inquilino
SET app.current_tenant = '123';

2. Filtragem no Nível da Aplicação

Sempre filtre por tenant_id no código da aplicação. Os exemplos abaixo usam GORM, mas diferentes ORMs têm suas próprias abordagens para construção de consultas. Para orientação sobre escolher o ORM certo para sua aplicação multi-inquilino, consulte nossa comparação de ORMs Go.

// ❌ RUIM - Filtro de inquilino ausente
db.Where("email = ?", email).First(&user)

// ✅ BOM - Sempre inclua o filtro de inquilino
db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user)

// ✅ MELHOR - Use escopos ou middleware
db.Scopes(TenantScope(tenantID)).Where("email = ?", email).First(&user)

3. Pooling de Conexão

Use poolers de conexão que suportam contexto de inquilino:

// PgBouncer com pooling de transação
// Ou use roteamento de conexão no nível da aplicação

4. Registros de Auditoria

Rastreie todo o acesso a dados de inquilino:

type AuditLog struct {
    ID        uint
    TenantID  uint
    UserID    uint
    Action    string
    Table     string
    RecordID  uint
    Timestamp time.Time
    IPAddress string
}

Otimização de Desempenho

Estratégia de Índices

O índice adequado é crucial para o desempenho do banco de dados multi-inquilino. Compreender estratégias de índice SQL, incluindo índices compostos e parciais, é essencial. Para uma referência abrangente sobre comandos SQL, incluindo CREATE INDEX e otimização de consultas, veja nossa SQL Cheatsheet. Para recursos específicos de índice do PostgreSQL e ajuste de desempenho, consulte nossa PostgreSQL Cheatsheet.

-- Índices compostos para consultas de inquilino
CREATE INDEX idx_orders_tenant_created ON orders(tenant_id, created_at DESC);
CREATE INDEX idx_orders_tenant_status ON orders(tenant_id, status);

-- Índices parciais para consultas comuns específicas de inquilino
CREATE INDEX idx_orders_active_tenant ON orders(tenant_id, created_at)
WHERE status = 'active';

Otimização de Consultas

// Use instruções preparadas para consultas de inquilino
stmt := db.Prepare("SELECT * FROM users WHERE tenant_id = $1 AND email = $2")

// Operações em lote por inquilino
db.Where("tenant_id = ?", tenantID).Find(&users)

// Use pooling de conexão por inquilino (para padrão de banco de dados separado)

Monitoramento

Ferramentas essenciais de gerenciamento de banco de dados são necessárias para monitorar aplicações multi-inquilino. Você precisará rastrear o desempenho das consultas, uso de recursos e saúde do banco de dados em todos os inquilinos. Para comparar ferramentas de gerenciamento de banco de dados que podem ajudar com isso, consulte nossa comparação entre DBeaver e Beekeeper. Ambas as ferramentas oferecem excelentes recursos para gerenciar e monitorar bancos de dados PostgreSQL em ambientes multi-inquilino.

Monitore métricas por inquilino:

  • Desempenho de consultas por inquilino
  • Uso de recursos por inquilino
  • Contagem de conexões por inquilino
  • Tamanho do banco de dados por inquilino

Estratégia de Migração

Padrão de Esquema Compartilhado

Ao implementar migrações de banco de dados, sua escolha de ORM afeta como você lida com alterações de esquema. Os exemplos abaixo usam a funcionalidade AutoMigrate do GORM, mas diferentes ORMs têm diferentes estratégias de migração. Para informações detalhadas sobre como diferentes ORMs Go lidam com migrações e gerenciamento de esquema, veja nossa comparação de ORMs Go.

// Migrações aplicam-se automaticamente a todos os inquilinos
func Migrate(db *gorm.DB) error {
    return db.AutoMigrate(&User{}, &Order{}, &Product{})
}

Padrão de Esquema/Banco de Dados Separado

// Migrações devem ser executadas por inquilino
func MigrateAllTenants(tenantIDs []uint) error {
    for _, tenantID := range tenantIDs {
        db := GetTenantDB(tenantID)
        if err := db.AutoMigrate(&User{}, &Order{}); err != nil {
            return fmt.Errorf("tenant %d: %w", tenantID, err)
        }
    }
    return nil
}

Matriz de Decisão

Fator Esquema Compartilhado Esquema Separado Banco de Dados Separado
Isolamento Baixo Médio Alto
Custo Baixo Médio Alto
Escalabilidade Alta Média Baixa-Média
Personalização Nenhum Médio Alto
Complexidade Operacional Baixa Média Alta
Conformidade Limitada Boa Excelente
Melhor Número de Inquilinos 1000+ 10-1000 1-100

Abordagem Híbrida

Você pode combinar padrões para diferentes níveis de inquilino:

// Inquilinos pequenos: Esquema compartilhado
if tenant.Tier == "standard" {
    return GetSharedDB(tenant.ID)
}

// Inquilinos empresariais: Banco de dados separado
if tenant.Tier == "enterprise" {
    return GetTenantDB(tenant.ID)
}

Boas Práticas

  1. Sempre filtre por inquilino: Nunca confie apenas no código da aplicação; use RLS quando possível. Compreender fundamentos do SQL ajuda a garantir a construção correta das consultas—consulte nossa SQL Cheatsheet para melhores práticas de consulta.
  2. Monitore o uso de recursos dos inquilinos: Identifique e limite inquilinos barulhentos. Use ferramentas de gerenciamento de banco de dados, como as comparadas em nossa guia DBeaver vs Beekeeper, para rastrear métricas de desempenho.
  3. Implemente middleware de contexto de inquilino: Centralize a extração e validação do inquilino. Sua escolha de ORM afeta como você implementa isso—veja nossa comparação de ORMs Go para diferentes abordagens.
  4. Use pooling de conexão: Gerencie eficientemente as conexões do banco de dados. Estratégias específicas de pooling de conexão do PostgreSQL são cobertas em nossa PostgreSQL Cheatsheet.
  5. Planeje a migração de inquilino: Capacidade de mover inquilinos entre padrões
  6. Implemente exclusão suave: Use deleted_at em vez de exclusões duras para dados de inquilino
  7. Audite tudo: Registre todos os acessos de dados de inquilino para conformidade
  8. Teste isolamento: Auditorias de segurança regulares para prevenir vazamento de dados entre inquilinos

Conclusão

Escolher o padrão correto de banco de dados multi-inquilino depende de seus requisitos específicos para isolamento, custo, escalabilidade e complexidade operacional. O padrão Banco de Dados Compartilhado, Esquema Compartilhado funciona bem para a maioria das aplicações SaaS, enquanto o Banco de Dados Separado por Inquilino é necessário para clientes empresariais com requisitos rigorosos de conformidade.

Comece com o padrão mais simples que atenda aos seus requisitos e planeje a migração para um padrão mais isolado conforme suas necessidades evoluam. Sempre priorize segurança e isolamento de dados, independentemente do padrão escolhido.