Multi-Tenancy-Datenbankmuster mit Beispielen in Go

Vollständiger Leitfaden zu Multi-Tenancy-Datenbankmustern

Inhaltsverzeichnis

Multi-Tenancy ist ein grundlegendes Architektur-Muster für SaaS-Anwendungen, das mehreren Kunden (Mietern) ermöglicht, dieselbe Anwendungsinfrastruktur zu teilen, während die Datenisolation aufrechterhalten wird.

Die Wahl des richtigen Datenbankmusters ist entscheidend für Skalierbarkeit, Sicherheit und betriebliche Effizienz.

databases-scheme

Überblick über Multi-Tenancy-Muster

Bei der Gestaltung einer Multi-Tenant-Anwendung haben Sie drei primäre Datenbankarchitektur-Muster zur Auswahl:

  1. Gemeinsame Datenbank, Gemeinsames Schema (am häufigsten verwendet)
  2. Gemeinsame Datenbank, Separate Schemas
  3. Separate Datenbank pro Mieter

Jedes Muster hat unterschiedliche Merkmale, Kompromisse und Anwendungsfälle. Lassen Sie uns jedes im Detail erkunden.

Muster 1: Gemeinsame Datenbank, Gemeinsames Schema

Dies ist das häufigste Multi-Tenancy-Muster, bei dem alle Mieter dieselbe Datenbank und dasselbe Schema teilen, wobei eine tenant_id-Spalte verwendet wird, um die Daten der Mieter zu unterscheiden.

Architektur

┌─────────────────────────────────────┐
│     Einzige Datenbank               │
│  ┌───────────────────────────────┐  │
│  │  Gemeinsames Schema           │  │
│  │  - users (tenant_id, ...)     │  │
│  │  - orders (tenant_id, ...)    │  │
│  │  - products (tenant_id, ...)  │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

Implementierungsbeispiel

Bei der Implementierung von Multi-Tenant-Mustern ist das Verständnis der SQL-Grundlagen entscheidend. Für eine umfassende Referenz zu SQL-Befehlen und Syntax besuchen Sie unseren SQL Cheatsheet. Hier ist, wie Sie das gemeinsame Schema-Muster einrichten:

-- Benutzertabelle mit 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)
);

-- Index auf tenant_id für Performance
CREATE INDEX idx_users_tenant_id ON users(tenant_id);

-- Zeilenebenen-Sicherheit (PostgreSQL-Beispiel)
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

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

Für weitere PostgreSQL-spezifische Funktionen und Befehle, einschließlich RLS-Richtlinien, Schema-Verwaltung und Performance-Optimierung, konsultieren Sie unseren PostgreSQL Cheatsheet.

Filterung auf Anwendungsebene

Bei der Arbeit mit Go-Anwendungen kann die Wahl des richtigen ORMs Ihre Multi-Tenant-Implementierung erheblich beeinflussen. Die folgenden Beispiele verwenden GORM, aber es gibt mehrere hervorragende Optionen verfügbar. Für einen detaillierten Vergleich von Go-ORMs, einschließlich GORM, Ent, Bun und sqlc, sehen Sie unseren umfassenden Leitfaden zu Go-ORMs für PostgreSQL.

// Beispiel in Go mit 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 zum Setzen des Mieterkontexts
func TenantMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tenantID := extractTenantID(r) // Aus Subdomain, Header oder JWT
        ctx := context.WithValue(r.Context(), "tenant_id", tenantID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Vorteile des gemeinsamen Schemas

  • Geringste Kosten: Einzige Datenbankinstanz, minimale Infrastruktur
  • Einfachste Operationen: Eine Datenbank zum Sichern, Überwachen und Warten
  • Einfache Schema-Änderungen: Migrationsanwendungen auf alle Mieter gleichzeitig
  • Am besten für hohe Mieteranzahl: Effiziente Ressourcennutzung
  • Cross-Tenant-Analysen: Einfache Aggregation von Daten über Mieter hinweg

Nachteile des gemeinsamen Schemas

  • Schwächere Isolation: Risiko von Datenlecks, wenn Abfragen den tenant_id-Filter vergessen
  • Lärmiger Nachbar: Die Arbeitslast eines Mieters kann andere beeinflussen
  • Begrenzte Anpassung: Alle Mieter teilen dasselbe Schema
  • Compliance-Herausforderungen: Schwerer, strenge Datenisolationanforderungen zu erfüllen
  • Backup-Komplexität: Kann einzelne Mieterdaten nicht leicht wiederherstellen

Gemeinsames Schema am besten für

  • SaaS-Anwendungen mit vielen kleinen bis mittelgroßen Mietern
  • Anwendungen, bei denen Mieter keine benutzerdefinierten Schemas benötigen
  • Kostenbewusste Startups
  • Wenn die Mieteranzahl hoch ist (tausende+)

Muster 2: Gemeinsame Datenbank, Separate Schemas

Jeder Mieter erhält sein eigenes Schema innerhalb derselben Datenbank, was eine bessere Isolation bietet, während die Infrastruktur geteilt wird.

Architektur mit separaten Schemas

┌─────────────────────────────────────┐
│     Einzige Datenbank               │
│  ┌──────────┐  ┌──────────┐         │
│  │ Schema A │  │ Schema B │  ...    │
│  │ (Mieters1)│  │ (Mieters2)│         │
│  └──────────┘  └──────────┘         │
└─────────────────────────────────────┘

Implementierung mit separaten Schemas

PostgreSQL-Schemas sind ein mächtiges Feature für Multi-Tenancy. Für detaillierte Informationen zur PostgreSQL-Schema-Verwaltung, Verbindungszeichenfolgen und Datenbankadministrationsbefehlen konsultieren Sie unseren PostgreSQL Cheatsheet.

-- Schema für Mieter erstellen
CREATE SCHEMA tenant_123;

-- Suchpfad für Mieteroperationen setzen
SET search_path TO tenant_123, public;

-- Tabellen im Mieter-Schema erstellen
CREATE TABLE tenant_123.users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255),
    created_at TIMESTAMP DEFAULT NOW()
);

Verbindungsverwaltung der Anwendung

Die effiziente Verwaltung von Datenbankverbindungen ist entscheidend für Multi-Tenant-Anwendungen. Der folgende Verbindungsverwaltungs-Code verwendet GORM, aber Sie möchten möglicherweise andere ORM-Optionen erkunden. Für einen gründlichen Vergleich von Go-ORMs, einschließlich Verbindungs-Pooling, Performance-Merkmalen und Anwendungsfällen, konsultieren Sie unseren Go-ORMs-Vergleichsleitfaden.

// Verbindungszeichenfolge mit Suchpfad
func GetTenantDB(tenantID uint) *gorm.DB {
    db := initializeDB()
    db.Exec(fmt.Sprintf("SET search_path TO tenant_%d, public", tenantID))
    return db
}

// Oder PostgreSQL-Verbindungszeichenfolge verwenden
// postgresql://user:pass@host/db?search_path=tenant_123

Vorteile separater Schemas

  • Bessere Isolation: Schema-Ebenen-Trennung reduziert das Risiko von Datenlecks
  • Anpassung: Jeder Mieter kann unterschiedliche Tabellenstrukturen haben
  • Mäßiger Kostenaufwand: Immer noch eine einzige Datenbankinstanz
  • Einfachere Mieter-Backups: Einzelne Schemas können gesichert werden
  • Besser für Compliance: Stärker als das gemeinsame Schema-Muster

Nachteile separater Schemas

  • Schema-Verwaltungskomplexität: Migrationsanwendungen müssen pro Mieter durchgeführt werden
  • Verbindungsüberhead: Suchpfad muss pro Verbindung gesetzt werden
  • Begrenzte Skalierbarkeit: Schema-Anzahl begrenzt (PostgreSQL ~10k Schemas)
  • Cross-Tenant-Abfragen: Komplexer, erfordert dynamische Schema-Referenzen
  • Ressourcengrenzen: Immer noch geteilte Datenbankressourcen

Separate Schemas am besten für

  • Mittelgroße SaaS-Anwendungen (Dutzende bis Hunderte von Mietern)
  • Wenn Mieter Schema-Anpassungen benötigen
  • Anwendungen, die eine bessere Isolation als das gemeinsame Schema benötigen
  • Wenn die Compliance-Anforderungen moderat sind

Muster 3: Separate Database per Tenant

Jeder Mieter erhält seine eigene vollständige Datenbankinstanz, was maximale Isolation bietet.

Separate Database Architektur

┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│  Datenbank 1 │  │  Datenbank 2  │  │  Datenbank 3  │
│  (Mietender A)  │  │  (Mietender B)  │  │  (Mietender C)  │
└──────────────┘  └──────────────┘  └──────────────┘

Separate Database Implementierung

-- Erstellen Sie eine Datenbank für den Mieter
CREATE DATABASE tenant_enterprise_corp;

-- Verbinden Sie sich mit der Mieterdatenbank
\c tenant_enterprise_corp

-- Erstellen Sie Tabellen (keine tenant_id erforderlich!)
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255),
    created_at TIMESTAMP DEFAULT NOW()
);

Dynamische Verbindungsverwaltung

// Verbindungspool-Manager
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()

    // Doppelprüfung nach Erhalt des Schreibschlosses
    if db, exists := m.pools[tenantID]; exists {
        return db, nil
    }

    // Neue Verbindung erstellen
    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
}

Separate Database Vorteile

  • Maximale Isolation: Vollständige Datentrennung
  • Beste Sicherheit: Kein Risiko von Quer-Mieter-Datenzugriff
  • Vollständige Anpassung: Jeder Mieter kann vollständig unterschiedliche Schemata haben
  • Unabhängige Skalierung: Skalieren Sie Mieterdatenbanken individuell
  • Einfache Compliance: Erfüllt strengste Datentrennungsanforderungen
  • Mieter-spezifische Backups: Einfache, unabhängige Sicherung/Wiederherstellung
  • Keine störenden Nachbarn: Mieter-Arbeitslasten beeinflussen sich nicht gegenseitig

Separate Database Nachteile

  • Höchste Kosten: Mehrere Datenbankinstanzen erfordern mehr Ressourcen
  • Betriebliche Komplexität: Verwaltung vieler Datenbanken (Sicherungen, Überwachung, Migrationen)
  • Verbindungslimits: Jede Datenbankinstanz hat Verbindungslimits
  • Quer-Mieter-Analysen: Erfordert Datenföderation oder ETL
  • Migrationskomplexität: Migrationen müssen über alle Datenbanken hinweg ausgeführt werden
  • Ressourcenüberhead: Mehr Speicher, CPU und Speicherplatz erforderlich

Separate Database Am Besten Für

  • Enterprise SaaS mit hochwertigen Kunden
  • Strenge Compliance-Anforderungen (HIPAA, GDPR, SOC 2)
  • Wenn Mieter erhebliche Anpassungen benötigen
  • Geringe bis mittlere Mieteranzahl (Dutzende bis niedrige Hunderte)
  • Wenn Mieter sehr unterschiedliche Datenmodelle haben

Sicherheitsüberlegungen

Unabhängig vom gewählten Muster ist Sicherheit von größter Bedeutung:

1. Zeilenebenen-Sicherheit (RLS)

PostgreSQL RLS filtert automatisch Abfragen nach Mieter und bietet eine Sicherheitsebene auf Datenbankebene. Dieses Feature ist besonders leistungsfähig für Multi-Tenant-Anwendungen. Weitere Details zu PostgreSQL RLS, Sicherheitsrichtlinien und anderen fortgeschrittenen PostgreSQL-Features finden Sie in unserem PostgreSQL Cheatsheet.

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

-- Richtlinie zur Isolation nach Mieter
CREATE POLICY tenant_isolation ON orders
    FOR ALL
    USING (tenant_id = current_setting('app.current_tenant')::INTEGER);

-- Anwendung setzt Mieterkontext
SET app.current_tenant = '123';

2. Filterung auf Anwendungsebene

Filtern Sie immer nach tenant_id im Anwendungscode. Die folgenden Beispiele verwenden GORM, aber verschiedene ORMs haben eigene Ansätze zum Abfrageaufbau. Für Anleitungen zur Auswahl des richtigen ORMs für Ihre Multi-Tenant-Anwendung überprüfen Sie unseren Vergleich von Go ORMs.

// ❌ SCHLECHT - Fehlender Mieterfilter
db.Where("email = ?", email).First(&user)

// ✅ GUT - Immer Mieterfilter einschließen
db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user)

// ✅ BESSER - Verwenden Sie Scopes oder Middleware
db.Scopes(TenantScope(tenantID)).Where("email = ?", email).First(&user)

3. Verbindungspooling

Verwenden Sie Verbindungspooler, die Mieterkontext unterstützen:

// PgBouncer mit Transaktionspooling
// Oder verwenden Sie die Verbindungsrouting auf Anwendungsebene

4. Audit-Logging

Protokollieren Sie alle Mieterdatenzugriffe:

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

Leistungsoptimierung

Indexierungsstrategie

Eine ordnungsgemäße Indexierung ist entscheidend für die Leistung von Multi-Tenant-Datenbanken. Das Verständnis von SQL-Indexierungsstrategien, einschließlich zusammengesetzter und partieller Indizes, ist unerlässlich. Für eine umfassende Referenz zu SQL-Befehlen, einschließlich CREATE INDEX und Abfrageoptimierung, sehen Sie unseren SQL Cheatsheet. Für PostgreSQL-spezifische Indexierungsmerkmale und Leistungsoptimierung beziehen Sie sich auf unseren PostgreSQL Cheatsheet.

-- Zusammengesetzte Indizes für Mieterabfragen
CREATE INDEX idx_orders_tenant_created ON orders(tenant_id, created_at DESC);
CREATE INDEX idx_orders_tenant_status ON orders(tenant_id, status);

-- Partielle Indizes für häufige mieterspezifische Abfragen
CREATE INDEX idx_orders_active_tenant ON orders(tenant_id, created_at)
WHERE status = 'active';

Abfrageoptimierung

// Verwenden Sie vorbereitete Anweisungen für Mieterabfragen
stmt := db.Prepare("SELECT * FROM users WHERE tenant_id = $1 AND email = $2")

// Batch-Operationen pro Mieter
db.Where("tenant_id = ?", tenantID).Find(&users)

// Verwenden Sie Verbindungspooling pro Mieter (für das Muster separate Datenbank)

Überwachung

Effektive Datenbankverwaltungs-Tools sind entscheidend für die Überwachung von Multi-Tenant-Anwendungen. Sie müssen die Abfrageleistung, den Ressourcenverbrauch und den Datenbankzustand über alle Mieter hinweg verfolgen. Zum Vergleich von Datenbankverwaltungs-Tools, die dabei helfen können, sehen Sie unsere DBeaver vs Beekeeper Vergleich. Beide Tools bieten hervorragende Funktionen zur Verwaltung und Überwachung von PostgreSQL-Datenbanken in Multi-Tenant-Umgebungen.

Überwachen Sie Mieter-Metriken:

  • Abfrageleistung pro Mieter
  • Ressourcenverbrauch pro Mieter
  • Verbindungszählungen pro Mieter
  • Datenbankgröße pro Mieter

Migrationsstrategie

Shared Schema Muster

Bei der Implementierung von Datenbankmigrationen beeinflusst Ihre Wahl des ORMs, wie Sie Schemaänderungen behandeln. Die folgenden Beispiele verwenden das AutoMigrate-Feature von GORM, aber verschiedene ORMs haben unterschiedliche Migrationsstrategien. Für detaillierte Informationen darüber, wie verschiedene Go ORMs Migrationen und Schema-Management handhaben, sehen Sie unseren Go ORMs Vergleich.

// Migrationen werden automatisch auf alle Mieter angewendet
func Migrate(db *gorm.DB) error {
    return db.AutoMigrate(&User{}, &Order{}, &Product{})
}

Separate Schema/Datenbank Muster

// Migrationen müssen pro Mieter ausgeführt werden
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
}

Entscheidungsmatrix

Faktor Shared Schema Separate Schema Separate DB
Isolation Niedrig Mittel Hoch
Kosten Niedrig Mittel Hoch
Skalierbarkeit Hoch Mittel Niedrig-Mittel
Anpassung Keine Mittel Hoch
Betriebliche Komplexität Niedrig Mittel Hoch
Compliance Begrenzt Gut Hervorragend
Beste Mieteranzahl 1000+ 10-1000 1-100

Hybridansatz

Sie können Muster für verschiedene Mieterstufen kombinieren:

// Kleine Mieter: Shared Schema
if tenant.Tier == "standard" {
    return GetSharedDB(tenant.ID)
}

// Enterprise-Mieter: Separate Datenbank
if tenant.Tier == "enterprise" {
    return GetTenantDB(tenant.ID)
}

Best Practices

  1. Immer nach Mieter filtern: Vertrauen Sie niemals nur dem Anwendungscode; verwenden Sie RLS, wenn möglich. Das Verständnis der SQL-Grundlagen hilft, eine ordnungsgemäße Abfragekonstruktion sicherzustellen – beziehen Sie sich auf unseren SQL Cheatsheet für Abfrage-Best Practices.
  2. Mieter-Ressourcennutzung überwachen: Identifizieren und drosseln Sie störende Nachbarn. Verwenden Sie Datenbankverwaltungs-Tools wie diejenigen, die in unserer DBeaver vs Beekeeper Anleitung verglichen werden, um Leistungsmetriken zu verfolgen.
  3. Implementieren Sie Mieterkontext-Middleware: Zentralisieren Sie die Mieter-Extraktion und -Validierung. Ihre ORM-Wahl beeinflusst, wie Sie dies implementieren – sehen Sie unseren Go ORMs Vergleich für verschiedene Ansätze.
  4. Verwenden Sie Verbindungspooling: Verwalten Sie Datenbankverbindungen effizient. PostgreSQL-spezifische Verbindungspooling-Strategien werden in unserem PostgreSQL Cheatsheet behandelt.
  5. Planen Sie Mieter-Migration: Fähigkeit, Mieter zwischen Mustern zu bewegen
  6. Implementieren Sie Soft Delete: Verwenden Sie deleted_at anstelle von harten Löschungen für Mieterdaten
  7. Auditieren Sie alles: Protokollieren Sie alle Mieterdatenzugriffe für Compliance
  8. Testen Sie Isolation: Regelmäßige Sicherheitsaudits, um Quer-Mieter-Datenlecks zu verhindern

Fazit

Die Wahl des richtigen Multi-Tenancy-Datenbankmusters hängt von Ihren spezifischen Anforderungen an Isolation, Kosten, Skalierbarkeit und betrieblicher Komplexität ab. Das Muster Shared Database, Shared Schema eignet sich gut für die meisten SaaS-Anwendungen, während Separate Database per Tenant für Unternehmens Kunden mit strengen Compliance-Anforderungen notwendig ist.

Beginnen Sie mit dem einfachsten Muster, das Ihre Anforderungen erfüllt, und planen Sie die Migration zu einem isolierteren Muster, wenn sich Ihre Bedürfnisse weiterentwickeln. Priorisieren Sie immer Sicherheit und Datenisolation, unabhängig vom gewählten Muster.