Multi-Tenancy-Datenbankmuster mit Beispielen in Go
Vollständiger Leitfaden zu Multi-Tenancy-Datenbankmustern
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.

Überblick über Multi-Tenancy-Muster
Bei der Gestaltung einer Multi-Tenant-Anwendung haben Sie drei primäre Datenbankarchitektur-Muster zur Auswahl:
- Gemeinsame Datenbank, Gemeinsames Schema (am häufigsten verwendet)
- Gemeinsame Datenbank, Separate Schemas
- 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
- 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.
- 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.
- 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.
- Verwenden Sie Verbindungspooling: Verwalten Sie Datenbankverbindungen effizient. PostgreSQL-spezifische Verbindungspooling-Strategien werden in unserem PostgreSQL Cheatsheet behandelt.
- Planen Sie Mieter-Migration: Fähigkeit, Mieter zwischen Mustern zu bewegen
- Implementieren Sie Soft Delete: Verwenden Sie deleted_at anstelle von harten Löschungen für Mieterdaten
- Auditieren Sie alles: Protokollieren Sie alle Mieterdatenzugriffe für Compliance
- 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.
Nützliche Links
- PostgreSQL Row-Level Security Dokumentation
- Multi-Tenant SaaS-Datenbankarchitektur
- Design von Multi-Tenant-Datenbanken
- Vergleich von Go ORMs für PostgreSQL: GORM vs Ent vs Bun vs sqlc
- PostgreSQL Cheatsheet: Ein schneller Leitfaden für Entwickler
- DBeaver vs Beekeeper - SQL-Datenbankverwaltungstools
- SQL Cheatsheet - die nützlichsten SQL-Befehle