Multi-Tenancy Databasmodeller med exempel i Go

Komplett guide till flerklientdatabasmodeller

Sidinnehåll

Multi-tenancy är ett grundläggande arkitekturmönster för SaaS-applikationer, som tillåter flera kunder (hyresgäster) att dela samma applikationsinfrastruktur samtidigt som dataisolering upprätthålls.

Att välja rätt databasstruktur är avgörande för skalbarhet, säkerhet och operativ effektivitet.

databases-scheme

Översikt över multi-tenancy-mönster

När du designar en multi-tenant-applikation har du tre primära databasarkitekturmönster att välja mellan:

  1. Delad databas, delad schema (vanligast)
  2. Delad databas, separata schema
  3. Separat databas per hyresgäst

Varje mönster har distinkta egenskaper, avvägningar och användningsområden. Låt oss utforska var och en i detalj.

Mönster 1: Delad databas, delad schema

Detta är det vanligaste multi-tenancy-mönstret, där alla hyresgäster delar samma databas och schema, med en tenant_id-kolumn som används för att skilja hyresgästdata.

Arkitektur

┌─────────────────────────────────────┐
│     Enkel databas                   │
│  ┌───────────────────────────────┐  │
│  │  Delad schema                 │  │
│  │  - users (tenant_id, ...)     │  │
│  │  - orders (tenant_id, ...)    │  │
│  │  - products (tenant_id, ...)  │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

Implementationsexempel

När du implementerar multi-tenant-mönster är det viktigt att förstå SQL-grunderna. För en omfattande referens till SQL-kommandon och syntax, se vårt SQL Cheatsheet. Här är hur du konfigurerar delad schema:

-- Användartabell med 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 på tenant_id för prestanda
CREATE INDEX idx_users_tenant_id ON users(tenant_id);

-- Radnivåssäkerhet (PostgreSQL-exempel)
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 mer PostgreSQL-specifika funktioner och kommandon, inklusive RLS-policys, schemamhantering och prestandjustering, se vårt PostgreSQL Cheatsheet.

Applikationsnivåfiltrering

När du arbetar med Go-applikationer kan valet av ORM påverka din multi-tenant-implementering betydligt. Exempel nedan använder GORM, men det finns flera bra alternativ. För en detaljerad jämförelse av Go-ORM:ar inklusive GORM, Ent, Bun och sqlc, se vårt omfattande guide till Go-ORM:ar för PostgreSQL.

// Exempel i Go med 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 för att ställa in hyresgästkontext
func TenantMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tenantID := extractTenantID(r) // Från subdomän, header eller JWT
        ctx := context.WithValue(r.Context(), "tenant_id", tenantID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Fördelar med delad schema

  • Lägst kostnad: En enda databasinstans, minimal infrastruktur
  • Enklast att hantera: En databas att säkerhetskopiera, övervaka och underhålla
  • Enkla schemamändringar: Migrationer tillämpas på alla hyresgäster samtidigt
  • Bäst för hög hyresgästantal: Effektiv resursanvändning
  • Korshyresgästanalys: Enkelt att sammanställa data över hyresgäster

Nackdelar med delad schema

  • Svagare isolering: Risk för dataläckage om frågor glömmer tenant_id-filter
  • Bullrig granne: En hyresgästs tunga arbetsbelastning kan påverka andra
  • Begränsad anpassning: Alla hyresgäster delar samma schema
  • Kompatibilitetsutmaningar: Svårare att uppfylla strikta dataisoleringskrav
  • Säkerhetskopplingskomplexitet: Kan inte återställa enskild hyresgästdata enkelt

Delad schema passar bäst för

  • SaaS-applikationer med många små till medelstora hyresgäster
  • Applikationer där hyresgäster inte behöver anpassade schema
  • Kostnadskänsliga startups
  • När hyresgästantalet är högt (tusental+)

Mönster 2: Delad databas, separata schema

Varje hyresgäst får sitt eget schema inom samma databas, vilket ger bättre isolering samtidigt som infrastrukturen delas.

Separata schema-arkitektur

┌─────────────────────────────────────┐
│     Enkel databas                   │
│  ┌──────────┐  ┌──────────┐         │
│  │ Schema A │  │ Schema B │  ...    │
│  │ (Hyresgäst1)│  │ (Hyresgäst2)│         │
│  └──────────┘  └──────────┘         │
└─────────────────────────────────────┘

Implementering av separata schema

PostgreSQL-schema är en kraftfull funktion för multi-tenancy. För detaljerad information om PostgreSQL-schemamhantering, anslutningssträngar och databasadministrationskommandon, se vårt PostgreSQL Cheatsheet.

-- Skapa schema för hyresgäst
CREATE SCHEMA tenant_123;

-- Ställ in sökväg för hyresgästsoperationer
SET search_path TO tenant_123, public;

-- Skapa tabeller i hyresgästschema
CREATE TABLE tenant_123.users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255),
    created_at TIMESTAMP DEFAULT NOW()
);

Hantering av applikationsanslutningar

Effektiv hantering av databasanslutningar är avgörande för multi-tenant-applikationer. Koden för anslutningshantering nedan använder GORM, men du kanske vill utforska andra ORM-alternativ. För en grundlig jämförelse av Go-ORM:ar inklusive anslutningspooler, prestandegenskaper och användningsområden, se vårt guide till jämförelse av Go-ORM:ar.

// Anslutningssträng med schemasökväg
func GetTenantDB(tenantID uint) *gorm.DB {
    db := initializeDB()
    db.Exec(fmt.Sprintf("SET search_path TO tenant_%d, public", tenantID))
    return db
}

// Eller använd PostgreSQL-anslutningssträng
// postgresql://user:pass@host/db?search_path=tenant_123

Fördelar med separata schema

  • Bättre isolering: Schema-nivåseparation minskar risken för dataläckage
  • Anpassning: Varje hyresgäst kan ha olika tabellstrukturer
  • Måttlig kostnad: Fortfarande en enda databasinstans
  • Enklare säkerhetskopiering per hyresgäst: Kan säkerhetskopiera enskilda schema
  • Bättre för kompatibilitet: Starkare än delat schema-mönster

Nackdelar med separata schema

  • Schema-hanteringskomplexitet: Migrationer måste köras per hyresgäst
  • Anslutningsöverhead: Behöver ställa in search_path per anslutning
  • Begränsad skalbarhet: Schemaantal begränsningar (PostgreSQL ~10k schema)
  • Korshyresgästsfrågor: Mer komplexa, kräver dynamiska schema-referenser
  • Resursbegränsningar: Delade databasresurser

Separata schema passar bäst för

  • Medelstora SaaS (dussintal till hundratals hyresgäster)
  • När hyresgäster behöver schema-anpassning
  • Applikationer som behöver bättre isolering än delat schema
  • När kompatibilitetskrav är måttliga

Mönster 3: Separat Databas per Hyresgäst

Varje hyresgäst får sin egen kompletta databasinstans, vilket ger maximal isolering.

Separat Databasarkitektur

┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│  Database 1  │  │  Database 2  │  │  Database 3  │
│  (Hyresgäst A)  │  │  (Hyresgäst B)  │  │  (Hyresgäst C)  │
└──────────────┘  └──────────────┘  └──────────────┘

Implementering av separat databas

-- Skapa databas för hyresgäst
CREATE DATABASE hyresgast_enterprise_corp;

-- Anslut till hyresgästens databas
\c hyresgast_enterprise_corp

-- Skapa tabeller (ingen tenant_id behövs!)
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255),
    created_at TIMESTAMP DEFAULT NOW()
);

Dynamisk anslutningshantering

// Anslutningspoolhanterare
type HyresgastDBManager struct {
    pools map[uint]*gorm.DB
    mu    sync.RWMutex
}

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

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

    // Dubbelkontroll efter att ha fått skrivlås
    if db, exists := m.pools[hyresgastID]; exists {
        return db, nil
    }

    // Skapa ny anslutning
    db, err := gorm.Open(postgres.Open(fmt.Sprintf(
        "host=localhost user=dbuser password=dbpass dbname=hyresgast_%d sslmode=disable",
        hyresgastID,
    )), &gorm.Config{})

    if err != nil {
        return nil, err
    }

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

Fördelar med separat databas

  • Maximal isolering: Fullständig dataseparation
  • Bäst säkerhet: Ingen risk för korshyresgästdataåtkomst
  • Fullständig anpassning: Varje hyresgäst kan ha helt olika scheman
  • Oberoende skalbarhet: Skala hyresgästdatabaser individuellt
  • Enkelt att uppfylla krav: Uppfyller strängaste dataisoleringskrav
  • Hyresgästspecifika säkerhetskopior: Enkla, oberoende säkerhetskopieringar/återställningar
  • Inga störande grannar: Hyresgästarbetsbelastningar påverkar inte varandra

Nackdelar med separat databas

  • Högst kostnad: Flera databasinstanser kräver mer resurser
  • Operativ komplexitet: Hantera många databaser (säkerhetskopior, övervakning, migreringar)
  • Anslutningsbegränsningar: Varje databasinstans har anslutningsbegränsningar
  • Korshyresgästanalys: Kräver datafederation eller ETL
  • Migreringskomplexitet: Måste köra migreringar över alla databaser
  • Resursöverhead: Mer minne, CPU och lagring behövs

Separat databas passar bäst för

  • Enterprise SaaS med högvärdiga kunder
  • Stränga krav på efterlevnad (HIPAA, GDPR, SOC 2)
  • När hyresgäster behöver betydande anpassning
  • Låg till medelhög hyresgästantal (dussintal till låga hundratal)
  • När hyresgäster har mycket olika datamodeller

Säkerhetsöverväganden

Oavsett vilket mönster som väljs är säkerhet av största vikt:

1. Radnivåssäkerhet (RLS)

PostgreSQL RLS filtrerar automatiskt frågor efter hyresgäst, vilket ger ett säkerhetslager på databasenivå. Denna funktion är särskilt kraftfull för flerhyresgästsapplikationer. För mer information om PostgreSQL RLS, säkerhetspolicys och andra avancerade PostgreSQL-funktioner, se vårt PostgreSQL Cheatsheet.

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

-- Policy för isolering efter hyresgäst
CREATE POLICY hyresgast_isolering ON orders
    FOR ALL
    USING (tenant_id = current_setting('app.current_tenant')::INTEGER);

-- Applikationen sätter hyresgästkontext
SET app.current_tenant = '123';

2. Applikationsnivåfiltrering

Filtrera alltid efter hyresgast_id i applikationskoden. Exempel nedan använder GORM, men olika ORM:er har sina egna metoder för frågebyggande. För vägledning om att välja rätt ORM för din flerhyresgästsapplikation, se vårt jämförelse av Go ORM:er.

// ❌ DÅLIG - Saknar hyresgästfilter
db.Where("email = ?", email).First(&user)

// ✅ BRA - Inkludera alltid hyresgästfilter
db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user)

// ✅ BÄTTRE - Använd scopes eller middleware
db.Scopes(TenantScope(tenantID)).Where("email = ?", email).First(&user)

3. Anslutningspooling

Använd anslutningspooler som stöder hyresgästkontext:

// PgBouncer med transaktionspooling
// Eller använd anslutningsrutning på applikationsnivå

4. Revisionsloggning

Spåra all hyresgästdataåtkomst:

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

Prestandaoptimering

Indexeringsstrategi

Rätt indexering är avgörande för flerhyresgästdatabasprestanda. Att förstå SQL-indexeringsstrategier, inklusive sammansatta index och partiella index, är avgörande. För en omfattande referens till SQL-kommandon inklusive CREATE INDEX och frågeoptimering, se vårt SQL Cheatsheet. För PostgreSQL-specifika indexeringsfunktioner och prestandajustering, se vårt PostgreSQL Cheatsheet.

-- Sammansatta index för hyresgästfrågor
CREATE INDEX idx_orders_tenant_created ON orders(tenant_id, created_at DESC);
CREATE INDEX idx_orders_tenant_status ON orders(tenant_id, status);

-- Partiella index för vanliga hyresgästspecifika frågor
CREATE INDEX idx_orders_active_tenant ON orders(tenant_id, created_at)
WHERE status = 'active';

Frågeoptimering

// Använd förberedda uttalanden för hyresgästfrågor
stmt := db.Prepare("SELECT * FROM users WHERE tenant_id = $1 AND email = $2")

// Batchoperationer per hyresgäst
db.Where("tenant_id = ?", tenantID).Find(&users)

// Använd anslutningspooling per hyresgäst (för separat databas-mönster)

Övervakning

Effektiva databasadministrationsverktyg är avgörande för övervakning av flerhyresgästsapplikationer. Du kommer att behöva spåra frågeprestanda, resursanvändning och databasstatus över alla hyresgäster. För att jämföra databasadministrationsverktyg som kan hjälpa till med detta, se vårt DBeaver vs Beekeeper-jämförelse. Båda verktygen erbjuder utmärkt funktioner för att hantera och övervaka PostgreSQL-databaser i flerhyresgästsmiljöer.

Övervaka hyresgästspecifika mätvärden:

  • Frågeprestanda per hyresgäst
  • Resursanvändning per hyresgäst
  • Anslutningsantal per hyresgäst
  • Databastorlek per hyresgäst

Migreringsstrategi

Delat schema-mönster

När du implementerar databasmigreringar påverkar ditt val av ORM hur du hanterar schemapåverkan. Exempel nedan använder GORM:s AutoMigrate-funktion, men olika ORM:er har olika migreringsstrategier. För detaljerad information om hur olika Go ORM:er hanterar migreringar och scheman, se vårt jämförelse av Go ORM:er.

// Migreringar tillämpas automatiskt på alla hyresgäster
func Migrate(db *gorm.DB) error {
    return db.AutoMigrate(&User{}, &Order{}, &Product{})
}

Separat schema/databas-mönster

// Migreringar måste köras per hyresgäst
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
}

Beslutsmatris

Faktor Delat Schema Separat Schema Separat DB
Isolering Låg Medel Hög
Kostnad Låg Medel Hög
Skalbarhet Hög Medel Låg-Medel
Anpassning Ingen Medel Hög
Operativ komplexitet Låg Medel Hög
Efterlevnad Begränsad Bra Utmärkt
Bäst antal hyresgäster 1000+ 10-1000 1-100

Hybridlösning

Du kan kombinera mönster för olika hyresgästnivåer:

// Små hyresgäster: Delat schema
if tenant.Tier == "standard" {
    return GetSharedDB(tenant.ID)
}

// Enterprise-hyresgäster: Separat databas
if tenant.Tier == "enterprise" {
    return GetTenantDB(tenant.ID)
}

Bästa praxis

  1. Filtrera alltid efter hyresgäst: Lita aldrig enbart på applikationskoden; använd RLS när möjligt. Att förstå SQL-fundamenten hjälper till att säkerställa korrekt frågekonstruktion - se vårt SQL Cheatsheet för frågebästa praxis.
  2. Övervaka hyresgästresursanvändning: Identifiera och begränsa störande grannar. Använd databasadministrationsverktyg som de som jämförs i vårt DBeaver vs Beekeeper-guide för att spåra prestandamätvärden.
  3. Implementera hyresgästkontextmiddleware: Centralisera extrahering och validering av hyresgäst. Ditt ORM-val påverkar hur du implementerar detta - se vårt jämförelse av Go ORM:er för olika tillvägagångssätt.
  4. Använd anslutningspooling: Effektivt hantera databasanslutningar. PostgreSQL-specifika anslutningspoolingsstrategier täcks i vårt PostgreSQL Cheatsheet.
  5. Planera för hyresgästmigrering: Möjlighet att flytta hyresgäster mellan mönster
  6. Implementera mjuk borttagning: Använd deleted_at istället för hårda borttagningar för hyresgästdata
  7. Revisionslogga allt: Logga all hyresgästdataåtkomst för efterlevnad
  8. Testa isolering: Reguljära säkerhetsrevisioner för att förebygga korshyresgästdataläckage

Slutsats

Valet av rätt flerhyresgästdatabasmönster beror på dina specifika krav på isolering, kostnad, skalbarhet och operativ komplexitet. Delat databas-, delat schema-mönster fungerar bra för de flesta SaaS-applikationer, medan separat databas per hyresgäst är nödvändig för företagskunder med stränga efterlevnadskrav.

Börja med det enklaste mönstret som uppfyller dina krav, och planera för migrering till ett mer isolerat mönster när dina behov utvecklas. Prioritera alltid säkerhet och dataisolering, oavsett vilket mönster som väljs.

Användbara länkar