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

Guia completo para padrões de banco de dados de multi-tenancy

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 enquanto mantêm o isolamento de dados.

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

databases-scheme

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

Ao projetar uma aplicação multi-tenant, 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, trade-offs e casos de uso distintos. Vamos explorar cada um em detalhe.

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

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

Arquitetura

┌─────────────────────────────────────┐
│     Banco de Dados Único             │
│  ┌───────────────────────────────┐  │
│  │  Esquema Compartilhado         │  │
│  │  - users (tenant_id, ...)     │  │
│  │  - orders (tenant_id, ...)    │  │
│  │  - products (tenant_id, ...)  │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

Exemplo de Implementação

Ao implementar padrões multi-tenant, entender os fundamentos de SQL é crucial. Para uma referência abrangente sobre comandos e sintaxe SQL, confira nosso Resumo de Comandos SQL. Veja 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 Nível de Linha (exemplo 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 e comandos específicos do PostgreSQL, incluindo políticas RLS, gerenciamento de esquemas e ajuste de desempenho, consulte nosso Resumo de Comandos PostgreSQL.

Filtragem no Nível da Aplicação

Ao trabalhar com aplicações Go, escolher o ORM correto pode impactar significativamente sua implementação multi-tenant. Os exemplos abaixo usam GORM, mas há várias opções excelentes disponíveis. Para uma comparação detalhada dos ORMs Go, incluindo GORM, Ent, Bun e sqlc, veja nosso guia abrangente de 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 contexto do inquilino
func TenantMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tenantID := extractTenantID(r) // De subdomínio, header 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 de banco de dados, infraestrutura mínima
  • Operações mais fáceis: Um banco de dados para backup, monitoramento e manutenção
  • Mudanças de esquema simples: Migrações aplicadas a todos os inquilinos de uma vez
  • Melhor para alto número de inquilinos: Utilização eficiente de recursos
  • Análises cross-tenant: 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
  • Vizinho barulhento: A carga pesada de um inquilino pode afetar os outros
  • Personalização limitada: Todos os inquilinos compartilham o mesmo esquema
  • Desafios de conformidade: Mais difícil atender requisitos rigorosos de isolamento de dados
  • Complexidade de backup: Não é fácil restaurar dados de inquilinos individuais

Esquema Compartilhado é Melhor para

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

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

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

Arquitetura de Esquema Separado

┌─────────────────────────────────────┐
│     Banco de Dados Único             │
│  ┌──────────┐  ┌──────────┐         │
│  │ Schema A │  │ Schema B │  ...    │
│  │ (Tenant1)│  │ (Tenant2)│         │
│  └──────────┘  └──────────┘         │
└─────────────────────────────────────┘

Implementação de Esquema Separado

Os esquemas do PostgreSQL são um recurso poderoso para multi-tenancy. Para informações detalhadas sobre gerenciamento de esquemas PostgreSQL, strings de conexão e comandos de administração de banco de dados, consulte nosso Resumo de Comandos PostgreSQL.

-- Criar esquema para o inquilino
CREATE SCHEMA tenant_123;

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

-- Criar 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-tenant. 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 aprofundada dos ORMs Go, incluindo pooling de conexões, características de desempenho e casos de uso, consulte nosso 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 usar string de conexão PostgreSQL
// postgresql://user:pass@host/db?search_path=tenant_123

Vantagens do Esquema Separado

  • Melhor isolamento: Separação a nível de esquema reduz o risco de vazamento de dados
  • Personalização: Cada inquilino pode ter estruturas de tabelas diferentes
  • Custo moderado: Ainda é uma única instância de banco de dados
  • Backups por inquilino mais fáceis: Pode fazer backup de esquemas individuais
  • Melhor para conformidade: Mais forte 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: Limites na contagem de esquemas (PostgreSQL ~10k esquemas)
  • Consultas cross-tenant: Mais complexas, requer referências de esquema dinâmicas
  • Limites de recursos: Recursos de banco de dados ainda compartilhados

Esquema Separado é Melhor para

  • SaaS de escala média (dezenas a centenas de inquilinos)
  • Quando os inquilinos precisam de personalização de esquema
  • Aplicações que precisam de melhor isolamento 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, proporcionando isolamento máximo.

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

-- Criar banco de dados para o inquilino
CREATE DATABASE tenant_enterprise_corp;

-- Conectar ao banco de dados do inquilino
\c tenant_enterprise_corp

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

Gerenciamento de Conexão Dinâmico

// Gerenciador de pool de conexões
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()

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

    // Criar 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 do Banco de Dados Separado

  • Isolamento máximo: Separação completa de dados
  • Melhor segurança: Sem risco de acesso a dados cross-tenant
  • Personalização total: Cada inquilino pode ter esquemas completamente diferentes
  • Escalabilidade independente: Escalar bancos de dados de inquilinos individualmente
  • Conformidade fácil: Atende aos requisitos mais rigorosos de isolamento de dados
  • Backups por inquilino: Backup/restauração simples e independente
  • Sem vizinhos barulhentos: Cargas de trabalho de inquilinos não se afetam

Desvantagens do Banco de Dados Separado

  • Custo mais alto: Múltiplas instâncias de banco de dados requerem 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álises cross-tenant: 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

Banco de Dados Separado é Melhor para

  • SaaS Empresarial com clientes de alto valor
  • Requisitos de conformidade rigorosos (HIPAA, GDPR, SOC 2)
  • Quando os inquilinos precisam de personalização significativa
  • Contagem de inquilinos baixa a média (dezenas a baixas centenas)
  • 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 Nível de Linha (RLS)

O RLS do PostgreSQL filtra automaticamente consultas por inquilino, fornecendo uma camada de segurança a nível de banco de dados. Este recurso é particularmente poderoso para aplicações multi-tenant. Para mais detalhes sobre RLS do PostgreSQL, políticas de segurança e outros recursos avançados do PostgreSQL, veja nosso Resumo de Comandos PostgreSQL.

-- Habilitar 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);

-- Aplicação define 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ções sobre a escolha do ORM certo para sua aplicação multi-tenant, confira nossa comparação de ORMs Go.

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

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

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

3. Pooling de Conexões

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

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

4. Logs de Auditoria

Rastreie todo o acesso a dados do 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 Indexação

A indexação adequada é crucial para o desempenho do banco de dados multi-tenant. Compreender estratégias de indexação SQL, incluindo índices compostos e parciais, é essencial. Para uma referência abrangente sobre comandos SQL, incluindo CREATE INDEX e otimização de consultas, veja nosso Resumo de Comandos SQL. Para recursos de indexação específicos do PostgreSQL e ajuste de desempenho, consulte nosso Resumo de Comandos PostgreSQL.

-- Í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 do 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 eficazes de gerenciamento de banco de dados são essenciais para monitorar aplicações multi-tenant. Você precisará rastrear desempenho de 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, confira nossa comparação DBeaver vs Beekeeper. Ambas as ferramentas oferecem excelentes recursos para gerenciar e monitorar bancos de dados PostgreSQL em ambientes multi-tenant.

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 mudanças de esquema. Os exemplos abaixo usam o recurso AutoMigrate do GORM, mas diferentes ORMs têm diferentes estratégias de migração. Para informações detalhadas sobre como vários ORMs Go lidam com migrações e gerenciamento de esquema, veja nossa comparação de ORMs Go.

// Migrações aplicam-se a todos os inquilinos automaticamente
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édio Baixo-Médio
Personalização Nenhuma Médio Alta
Complexidade Operacional Baixo Médio Alto
Conformidade Limitado Bom Excelente
Melhor Contagem de Inquilinos 1000+ 10-1000 1-100

Abordagem Híbrida

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

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

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

Melhores Práticas

  1. Sempre filtre por inquilino: Nunca confie apenas no código da aplicação; use RLS quando possível. Entender os fundamentos de SQL ajuda a garantir a construção adequada de consultas — consulte nosso Resumo de Comandos SQL para melhores práticas de consulta.
  2. Monitore o uso de recursos do inquilino: Identifique e limite vizinhos barulhentos. Use ferramentas de gerenciamento de banco de dados como as comparadas em nosso guia DBeaver vs Beekeeper para rastrear métricas de desempenho.
  3. Implemente middleware de contexto do 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 conexões de banco de dados de forma eficiente. Estratégias de pooling de conexão específicas do PostgreSQL são cobertas em nosso Resumo de Comandos PostgreSQL.
  5. Planeje para migração de inquilinos: Capacidade de mover inquilinos entre padrões
  6. Implemente exclusão suave: Use deleted_at em vez de exclusões rígidas para dados do inquilino
  7. Audite tudo: Registre todo o acesso a dados do inquilino para conformidade
  8. Teste isolamento: Auditorias de segurança regulares para prevenir vazamento de dados cross-tenant

Conclusão

Escolher o padrão de banco de dados multi-tenant certo depende dos seus requisitos específicos de 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 Banco de Dados Separado por Inquilino é necessário para clientes empresariais com necessidades de conformidade rigorosas.

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 evoluem. Priorize sempre a segurança e o isolamento de dados, independentemente do padrão escolhido.

Assinar

Receba novos artigos sobre sistemas, infraestrutura e engenharia de IA.