Wzorce baz danych z wieloma dzierżawcami z przykładami w Go
Kompletny przewodnik po wzorcach baz danych wielodostępowych
Multi-tenancy to fundamentalny wzorzec architektoniczny dla aplikacji SaaS, umożliwiający wielu klientom (najemcom) współdzielone korzystanie z tej samej infrastruktury aplikacji, przy jednoczesnym utrzymaniu izolacji danych.
Wybór odpowiedniego wzorca bazy danych jest kluczowy dla skalowalności, bezpieczeństwa i wydajności operacyjnej.

Omówienie wzorców multi-tenancy
Podczas projektowania aplikacji multi-tenant, masz do wyboru trzy główne wzorce architektury bazy danych:
- Współdzielona baza danych, współdzielony schemat (najczęstszy)
- Współdzielona baza danych, osobny schemat
- Oddzielna baza danych dla każdego najemcy
Każdy wzorzec ma swoje charakterystyczne cechy, kompromisy i przypadki użycia. Przeanalizujmy je szczegółowo.
Wzorzec 1: Współdzielona baza danych, współdzielony schemat
To najbardziej powszechny wzorzec multi-tenancy, w którym wszyscy najemcy współdzielą tę samą bazę danych i schemat, z kolumną tenant_id używaną do odróżniania danych najemców.
Architektura
┌─────────────────────────────────────┐
│ Single Database │
│ ┌───────────────────────────────┐ │
│ │ Shared Schema │ │
│ │ - users (tenant_id, ...) │ │
│ │ - orders (tenant_id, ...) │ │
│ │ - products (tenant_id, ...) │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
Przykład implementacji
Podczas implementacji wzorców multi-tenancy, zrozumienie podstaw SQL jest kluczowe. Dla kompleksowego odniesienia do poleceń i składni SQL, sprawdź nasz SQL Cheatsheet. Oto jak skonfigurować wzorzec współdzielonego schematu:
-- Tabela użytkowników z kolumną 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)
);
-- Indeks na kolumnie tenant_id dla wydajności
CREATE INDEX idx_users_tenant_id ON users(tenant_id);
-- Zabezpieczenie na poziomie wiersza (przykład dla 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);
Dla więcej informacji na temat funkcji i poleceń PostgreSQL, w tym zasad RLS, zarządzania schematami i optymalizacji wydajności, odwiedź nasz PostgreSQL Cheatsheet.
Filtrowanie na poziomie aplikacji
Podczas pracy z aplikacjami w języku Go, wybór odpowiedniego ORM może znacząco wpłynąć na implementację multi-tenancy. Przykłady poniżej używają GORM, ale dostępne są również inne świetne opcje. Dla szczegółowego porównania ORMów w języku Go, w tym GORM, Ent, Bun i sqlc, zobacz nasz kompleksowy przewodnik po ORMach w języku Go dla PostgreSQL.
// Przykład w Go z użyciem 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
}
// Środek weryfikacji do ustawienia kontekstu najemcy
func TenantMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tenantID := extractTenantID(r) // Z poddomeny, nagłówka lub JWT
ctx := context.WithValue(r.Context(), "tenant_id", tenantID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Zalety współdzielonego schematu
- Najniższy koszt: Jedna instancja bazy danych, minimalna infrastruktura
- Najprostsze operacje: Jedna baza danych do kopii zapasowej, monitorowania i utrzymania
- Proste zmiany schematu: Migracje są stosowane do wszystkich najemców naraz
- Najlepsze dla dużej liczby najemców: Efektywne wykorzystanie zasobów
- Analiza między-najemcza: Łatwe agregowanie danych między najemcami
Wady współdzielonego schematu
- Słabsza izolacja: Ryzyko wycieku danych, jeśli zapytania zapomną filtrować po tenant_id
- Noisy neighbor: Ciężki obciążenie jednego najemcy może wpłynąć na innych
- Ograniczona personalizacja: Wszyscy najemcy współdzielą ten sam schemat
- Trudności w zgodności: Trudniej spełniać wymagania ścisłej izolacji danych
- Złożoność kopii zapasowej: Trudno łatwo przywrócić dane jednego najemcy
Najlepsze do użycia współdzielonego schematu
- Aplikacje SaaS z wieloma małymi i średnimi najemcami
- Aplikacje, w których najemcy nie potrzebują personalizowanych schematów
- Start-upy wrażliwe na koszty
- Gdy liczba najemców jest wysoka (tysiące+)
Wzorzec 2: Współdzielona baza danych, osobny schemat
Każdy najemca otrzymuje własny schemat w ramach tej samej bazy danych, zapewniając lepszą izolację, jednocześnie współdzieląc infrastrukturę.
Architektura osobnego schematu
┌─────────────────────────────────────┐
│ Single Database │
│ ┌──────────┐ ┌──────────┐ │
│ │ Schema A │ │ Schema B │ ... │
│ │ (Tenant1)│ │ (Tenant2)│ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────┘
Implementacja osobnego schematu
Schematy PostgreSQL to potężna funkcja dla multi-tenancy. Dla szczegółowych informacji na temat zarządzania schematami PostgreSQL, łańcuchów połączeń i poleceń administracyjnych bazy danych, skorzystaj z naszego PostgreSQL Cheatsheet.
-- Utwórz schemat dla najemcy
CREATE SCHEMA tenant_123;
-- Ustaw ścieżkę wyszukiwania dla operacji najemcy
SET search_path TO tenant_123, public;
-- Utwórz tabele w schemacie najemcy
CREATE TABLE tenant_123.users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
Zarządzanie połączeniami aplikacji
Efektywne zarządzanie połączeniami jest kluczowe dla aplikacji multi-tenant. Poniższy kod zarządzania połączeniami używa GORM, ale warto rozważyć inne opcje ORM. Dla szczegółowego porównania ORMów w języku Go, w tym zarządzania pulami połączeń, cech wydajnościowych i przypadków użycia, odwiedź nasz przewodnik po porównaniu ORMów w języku Go.
// Ciąg połączenia z ustawieniem ścieżki wyszukiwania
func GetTenantDB(tenantID uint) *gorm.DB {
db := initializeDB()
db.Exec(fmt.Sprintf("SET search_path TO tenant_%d, public", tenantID))
return db
}
// Albo użyj łańcucha połączenia PostgreSQL
// postgresql://user:pass@host/db?search_path=tenant_123
Zalety osobnego schematu
- Lepsza izolacja: Oddzielne schematy zmniejszają ryzyko wycieku danych
- Personalizacja: Każdy najemca może mieć różne struktury tabel
- Średni koszt: Nadal jedna instancja bazy danych
- Łatwe kopie zapasowe dla najemców: Można tworzyć kopie zapasowe poszczególnych schematów
- Lepsze dla zgodności: Silniejsze niż wzorzec współdzielonego schematu
Wady osobnego schematu
- Złożoność zarządzania schematami: Migracje muszą być uruchamiane dla każdego najemcy
- Nadmiarowe obciążenie połączeń: Musisz ustawić search_path dla każdego połączenia
- Ograniczona skalowalność: Liczba schematów ma ograniczenia (PostgreSQL ~10k schematów)
- Złożone zapytania między-najemcze: Wymagają dynamicznych odwołań do schematów
- Ograniczenia zasobów: Nadal współdzielone zasoby bazy danych
Najlepsze do użycia osobnego schematu
- SaaS o średniej skali (dziesiątki do setek najemców)
- Gdy najemcy potrzebują personalizacji schematu
- Aplikacje potrzebujące lepszej izolacji niż współdzielony schemat
- Gdy wymagania zgodności są średnie
Wzorzec 3: Oddzielna baza danych dla każdego najemcy
Każdy najemca otrzymuje własną pełną instancję bazy danych, zapewniając maksymalną izolację.
Architektura osobnej bazy danych
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Database 1 │ │ Database 2 │ │ Database 3 │
│ (Tenant A) │ │ (Tenant B) │ │ (Tenant C) │
└──────────────┘ └──────────────┘ └──────────────┘
Implementacja osobnej bazy danych
-- Utwórz bazę danych dla najemcy
CREATE DATABASE tenant_enterprise_corp;
-- Połącz się z bazą danych najemcy
\c tenant_enterprise_corp
-- Utwórz tabele (nie potrzebny tenant_id!)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
Dynamiczne zarządzanie połączeniami
// Menedżer puli połączeń
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()
// Sprawdź ponownie po uzyskaniu bloku zapisu
if db, exists := m.pools[tenantID]; exists {
return db, nil
}
// Utwórz nowe połączenie
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
}
Zalety osobnej bazy danych
- Maksymalna izolacja: Pełne oddzielenie danych
- Najlepsze bezpieczeństwo: Brak ryzyka dostępu do danych między najemcami
- Pełna personalizacja: Każdy najemca może mieć zupełnie inne schematy
- Indywidualne skalowanie: Możliwość skalowania baz danych najemców osobno
- Łatwe zgodność: Spełnia najbardziej rygorystyczne wymagania izolacji danych
- Kopie zapasowe dla najemców: Proste, niezależne kopie zapasowe i przywracanie
- Brak noisy neighbor: Obciążenie jednego najemcy nie wpływa na innych
Wady osobnej bazy danych
- Najwyższy koszt: Wiele instancji baz danych wymaga więcej zasobów
- Złożoność operacyjna: Zarządzanie wieloma bazami danych (kopie zapasowe, monitorowanie, migracje)
- Ograniczenia połączeń: Każda instancja bazy danych ma ograniczenia liczby połączeń
- Analiza między-najemcza: Wymaga federacji danych lub ETL
- Złożoność migracji: Musisz uruchomić migracje na wszystkich bazach danych
- Nadmiarowe obciążenie zasobów: Wymaga więcej pamięci, procesora i miejsca na dysku
Najlepsze do użycia osobnej bazy danych
- SaaS dla przedsiębiorstw z wysokowartościowymi klientami
- Ścisze wymagania zgodności (HIPAA, GDPR, SOC 2)
- Gdy najemcy potrzebują znacznej personalizacji
- Niski do średniego poziomu najemców (dziesiątki do niskich setek)
- Gdy najemcy mają bardzo różne modele danych
Aspekty bezpieczeństwa
Niezależnie od wybranego wzorca, bezpieczeństwo jest kluczowe:
1. Zabezpieczenie na poziomie wiersza (RLS)
RLS w PostgreSQL automatycznie filtrowa zapytania według najemcy, zapewniając warstwę bezpieczeństwa na poziomie bazy danych. Ta funkcja jest szczególnie potężna dla aplikacji multi-tenant. Dla więcej informacji na temat RLS w PostgreSQL, zasad bezpieczeństwa i innych zaawansowanych funkcji PostgreSQL, zobacz nasz PostgreSQL Cheatsheet.
-- Włącz RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Zasada izolacji według najemcy
CREATE POLICY tenant_isolation ON orders
FOR ALL
USING (tenant_id = current_setting('app.current_tenant')::INTEGER);
-- Aplikacja ustawia kontekst najemcy
SET app.current_tenant = '123';
2. Filtrowanie na poziomie aplikacji
Zawsze filtruj według tenant_id w kodzie aplikacji. Przykłady poniżej używają GORM, ale różne ORM-y mają swoje podejścia do budowania zapytań. Dla wskazówek dotyczących wyboru odpowiedniego ORM-a dla aplikacji multi-tenant, sprawdź nasz porównanie ORMów w języku Go.
// ❌ ZŁE - Brak filtra według najemcy
db.Where("email = ?", email).First(&user)
// ✅ DOBRE - Zawsze dodaj filtr według najemcy
db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user)
// ✅ Lepsze - Użyj zakresów lub middleware
db.Scopes(TenantScope(tenantID)).Where("email = ?", email).First(&user)
3. Pula połączeń
Używaj pulerów połączeń obsługujących kontekst najemcy:
// PgBouncer z pulą transakcji
// Albo użyj routingu połączeń na poziomie aplikacji
4. Rejestrowanie audytu
Śledź wszystkie dostęp do danych najemców:
type AuditLog struct {
ID uint
TenantID uint
UserID uint
Action string
Table string
RecordID uint
Timestamp time.Time
IPAddress string
}
Optymalizacja wydajności
Strategia indeksowania
Poprawne indeksowanie jest kluczowe dla wydajności baz danych multi-tenant. Zrozumienie strategii indeksowania SQL, w tym indeksów złożonych i częściowych, jest niezbędne. Dla kompleksowego odniesienia do poleceń SQL, w tym CREATE INDEX i optymalizacji zapytań, zobacz nasz SQL Cheatsheet. Dla funkcji indeksowania i optymalizacji wydajności specyficznych dla PostgreSQL, odwiedź nasz PostgreSQL Cheatsheet.
-- Złożone indeksy dla zapytań najemców
CREATE INDEX idx_orders_tenant_created ON orders(tenant_id, created_at DESC);
CREATE INDEX idx_orders_tenant_status ON orders(tenant_id, status);
-- Częściowe indeksy dla typowych zapytań specyficznych dla najemców
CREATE INDEX idx_orders_active_tenant ON orders(tenant_id, created_at)
WHERE status = 'active';
Optymalizacja zapytań
// Użyj przygotowanych zapytań dla zapytań najemców
stmt := db.Prepare("SELECT * FROM users WHERE tenant_id = $1 AND email = $2")
// Operacje wsadowe dla każdego najemcy
db.Where("tenant_id = ?", tenantID).Find(&users)
// Użyj puli połączeń dla każdego najemcy (dla wzorca osobnej bazy danych)
Monitorowanie
Efektywne narzędzia zarządzania bazą danych są niezbędne do monitorowania aplikacji multi-tenant. Musisz śledzić wydajność zapytań, wykorzystanie zasobów i stan bazy danych dla wszystkich najemców. Dla porównania narzędzi zarządzania bazą danych, które mogą pomóc w tym, sprawdź nasz porównanie DBeaver vs Beekeeper. Oba narzędzia oferują świetne funkcje do zarządzania i monitorowania baz danych PostgreSQL w środowiskach multi-tenant.
Monitoruj metryki dla każdego najemcy:
- Wydajność zapytań dla każdego najemcy
- Wykorzystanie zasobów dla każdego najemcy
- Liczba połączeń dla każdego najemcy
- Rozmiar bazy danych dla każdego najemcy
Strategia migracji
Wzorzec współdzielonego schematu
Podczas implementacji migracji bazy danych, wybór ORM-a wpływa na to, jak radzisz sobie z zmianami schematu. Przykłady poniżej używają funkcji AutoMigrate w GORM, ale różne ORM-y mają różne strategie migracji. Dla szczegółowych informacji na temat tego, jak różne ORM-y w języku Go radzą sobie z migracjami i zarządzaniem schematami, zobacz nasz porównanie ORMów w języku Go.
// Migracje są stosowane automatycznie dla wszystkich najemców
func Migrate(db *gorm.DB) error {
return db.AutoMigrate(&User{}, &Order{}, &Product{})
}
Wzorzec osobnego schematu/bazy danych
// Migracje muszą być uruchamiane dla każdego najemcy
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
}
Macierz decyzyjna
| Czynnik | Współdzielony schemat | Osobny schemat | Osobna baza danych |
|---|---|---|---|
| Izolacja | Niska | Średnia | Wysoka |
| Koszt | Niski | Średni | Wysoki |
| Skalowalność | Wysoka | Średnia | Niska-Średnia |
| Personalizacja | Brak | Średnia | Wysoka |
| Złożoność operacyjna | Niska | Średnia | Wysoka |
| Zgodność | Ograniczona | Dobra | Wspaniała |
| Najlepsza liczba najemców | 1000+ | 10-1000 | 1-100 |
Hybrydowy podejście
Możesz połączyć wzorce dla różnych poziomów najemców:
// Małe najemcy: Współdzielony schemat
if tenant.Tier == "standard" {
return GetSharedDB(tenant.ID)
}
// Najemcy enterprise: Osobna baza danych
if tenant.Tier == "enterprise" {
return GetTenantDB(tenant.ID)
}
Praktyczne wskazówki
- Zawsze filtruj według najemcy: Nie zaufaj tylko kodowi aplikacji; użyj RLS, jeśli to możliwe. Zrozumienie podstaw SQL pomaga zapewnić poprawne konstrukcje zapytań — odwiedź nasz SQL Cheatsheet dla najlepszych praktyk w zakresie zapytań.
- Monitoruj wykorzystanie zasobów przez najemców: Identyfikuj i ogranicz najemców generujących szum. Użyj narzędzi zarządzania bazą danych, takich jak te porównane w naszym przewodniku DBeaver vs Beekeeper, aby śledzić metryki wydajności.
- Zaimplementuj middleware do kontekstu najemcy: Centralizuj ekstrakcję i walidację najemcy. Wybór ORM-a wpływa na to, jak to zaimplementujesz — zobacz nasz porównanie ORMów w języku Go dla różnych podejść.
- Użyj puli połączeń: Efektywnie zarządzaj połączeniami do bazy danych. Strategie puli połączeń specyficzne dla PostgreSQL są omawiane w naszym PostgreSQL Cheatsheet.
- Planuj migrację najemców: Możliwość przenoszenia najemców między wzorcami
- Zaimplementuj miękkie usuwanie: Użyj deleted_at zamiast twardego usuwania danych najemców
- Rejestruj wszystko: Loguj wszystkie dostęp do danych najemców dla zgodności
- Testuj izolację: Regularne audyty bezpieczeństwa, aby zapobiec wyciekom danych między najemcami
Podsumowanie
Wybór odpowiedniego wzorca bazy danych multi-tenancy zależy od Twoich konkretnych wymagań dotyczących izolacji, kosztów, skalowalności i złożoności operacyjnej. Wzorzec Współdzielona baza danych, współdzielony schemat działa dobrze dla większości aplikacji SaaS, podczas gdy osobna baza danych dla każdego najemcy jest konieczna dla klientów enterprise z rygorystycznymi wymaganiami zgodności.
Zacznij od najprostszego wzorca, który spełnia Twoje wymagania, a planuj migrację do bardziej izolowanego wzorca, gdy Twoje potrzeby ewoluują. Zawsze priorytetyzuj bezpieczeństwo i izolację danych, niezależnie od wybranego wzorca.
Przydatne linki
- Dokumentacja PostgreSQL Row-Level Security
- Architektura bazy danych SaaS z wieloma najemcami
- Projektowanie baz danych multi-tenant
- Porównanie ORMów w języku Go dla PostgreSQL: GORM vs Ent vs Bun vs sqlc
- PostgreSQL Cheatsheet: Szybki przewodnik dla programisty
- DBeaver vs Beekeeper - Narzędzia zarządzania bazami danych SQL
- SQL Cheatsheet - najbardziej przydatne polecenia SQL