Шаблоны многоквартирных баз данных с примерами на Go
Полное руководство по шаблонам многоквартирных баз данных
Мультитенантность — это фундаментальный архитектурный паттерн для SaaS-приложений, позволяющий нескольким клиентам (арендаторам) использовать одну и ту же инфраструктуру приложения, сохраняя при этом изоляцию данных.
Выбор правильного паттерна базы данных критически важен для масштабируемости, безопасности и операционной эффективности.

Обзор паттернов мультитенантности
При проектировании мультитенантного приложения у вас есть три основных паттерна архитектуры базы данных на выбор:
- Общая база данных, общая схема (наиболее распространенный)
- Общая база данных, отдельные схемы
- Отдельная база данных на арендатора
Каждый паттерн имеет свои уникальные характеристики, компромиссы и области применения. Давайте подробно рассмотрим каждый из них.
Паттерн 1: Общая база данных, общая схема
Это наиболее распространенный паттерн мультитенантности, где все арендаторы используют одну и ту же базу данных и схему, с колонкой tenant_id, которая используется для различения данных арендаторов.
Архитектура
┌─────────────────────────────────────┐
│ Единая база данных │
│ ┌───────────────────────────────┐ │
│ │ Общая схема │ │
│ │ - users (tenant_id, ...) │ │
│ │ - orders (tenant_id, ...) │ │
│ │ - products (tenant_id, ...) │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
Пример реализации
При реализации паттернов мультитенантности понимание основ SQL критически важно. Для всестороннего справочника по командам и синтаксису SQL обратитесь к нашему SQL Cheatsheet. Вот как настроить паттерн общей схемы:
-- Таблица пользователей с 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)
);
-- Индекс на tenant_id для производительности
CREATE INDEX idx_users_tenant_id ON users(tenant_id);
-- Управление доступом на уровне строк (пример для 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);
Для более специфичных для PostgreSQL функций и команд, включая политики RLS, управление схемами и настройку производительности, обратитесь к нашему PostgreSQL Cheatsheet.
Фильтрация на уровне приложения
При работе с приложениями на Go выбор правильного ORM может значительно повлиять на реализацию мультитенантности. Примеры ниже используют GORM, но существует несколько отличных альтернатив. Для детального сравнения ORM для Go, включая GORM, Ent, Bun и sqlc, см. наше комплексное руководство по ORM для PostgreSQL.
// Пример на Go с 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 для установки контекста арендатора
func TenantMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tenantID := extractTenantID(r) // Из поддомена, заголовка или JWT
ctx := context.WithValue(r.Context(), "tenant_id", tenantID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Преимущества общей схемы
- Наименьшие затраты: одна инстанция базы данных, минимальная инфраструктура
- Проще операции: одна база данных для резервного копирования, мониторинга и обслуживания
- Простые изменения схемы: миграции применяются ко всем арендаторам сразу
- Лучше для большого количества арендаторов: эффективное использование ресурсов
- Аналитика между арендаторами: легко агрегировать данные по арендаторам
Недостатки общей схемы
- Слабая изоляция: риск утечки данных, если запросы забывают фильтр tenant_id
- Шумный сосед: нагрузка одного арендатора может повлиять на других
- Ограниченная настраиваемость: все арендаторы используют одну и ту же схему
- Сложности с соответствием требованиям: сложнее соответствовать строгим требованиям изоляции данных
- Сложность резервного копирования: нельзя легко восстановить данные отдельного арендатора
Общая схема лучше всего подходит для
- SaaS-приложений с большим количеством небольших и средних арендаторов
- Приложений, где арендаторам не нужны индивидуальные схемы
- Стартапов с ограниченным бюджетом
- Когда количество арендаторов велико (тысячи+)
Паттерн 2: Общая база данных, отдельные схемы
Каждый арендатор получает свою собственную схему в пределах одной и той же базы данных, обеспечивая лучшую изоляцию при совместном использовании инфраструктуры.
Архитектура отдельных схем
┌─────────────────────────────────────┐
│ Единая база данных │
│ ┌──────────┐ ┌──────────┐ │
│ │ Схема A │ │ Схема B │ ... │
│ │ (Арендатор1)│ │ (Арендатор2)│ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────┘
Реализация отдельных схем
Схемы PostgreSQL — мощная функция для мультитенантности. Для детальной информации о управлении схемами PostgreSQL, строках подключения и командах администрирования базы данных обратитесь к нашему PostgreSQL Cheatsheet.
-- Создание схемы для арендатора
CREATE SCHEMA tenant_123;
-- Установка пути поиска для операций арендатора
SET search_path TO tenant_123, public;
-- Создание таблиц в схеме арендатора
CREATE TABLE tenant_123.users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
Управление подключениями приложения
Эффективное управление подключениями к базе данных критически важно для мультитенантных приложений. Приведенный ниже код управления подключениями использует GORM, но вы можете захотеть исследовать другие варианты ORM. Для всестороннего сравнения ORM для Go, включая пулы подключений, характеристики производительности и области применения, обратитесь к нашему руководству по сравнению ORM для Go.
// Строка подключения с путем поиска схемы
func GetTenantDB(tenantID uint) *gorm.DB {
db := initializeDB()
db.Exec(fmt.Sprintf("SET search_path TO tenant_%d, public", tenantID))
return db
}
// Или использовать строку подключения PostgreSQL
// postgresql://user:pass@host/db?search_path=tenant_123
Преимущества отдельных схем
- Лучшая изоляция: разделение на уровне схемы снижает риск утечки данных
- Настраиваемость: каждый арендатор может иметь разные структуры таблиц
- Умеренные затраты: все еще одна инстанция базы данных
- Проще резервное копирование на арендатора: можно делать резервные копии отдельных схем
- Лучше для соответствия требованиям: сильнее, чем паттерн общей схемы
Недостатки отдельных схем
- Сложность управления схемами: миграции должны выполняться для каждого арендатора
- Накладные расходы на подключение: необходимо устанавливать search_path для каждого подключения
- Ограниченная масштабируемость: ограничение на количество схем (PostgreSQL ~10k схем)
- Запросы между арендаторами: более сложные, требуют динамических ссылок на схемы
- Ограничения ресурсов: все еще общие ресурсы базы данных
Отдельные схемы лучше всего подходят для
- Среднемасштабные SaaS (десятки до сотен арендаторов)
- Когда арендаторам нужна настраиваемость схемы
- Приложениям, которым нужна лучшая изоляция, чем общая схема
- Когда требования к соответствию умеренные
Шаблон 3: Отдельная база данных для каждого арендатора
Каждый арендатор получает собственную полную экземпляр базы данных, обеспечивая максимальную изоляцию.
Архитектура отдельных баз данных
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Database 1 │ │ Database 2 │ │ Database 3 │
│ (Tenant A) │ │ (Tenant B) │ │ (Tenant C) │
└──────────────┘ └──────────────┘ └──────────────┘
Реализация отдельных баз данных
-- Создание базы данных для арендатора
CREATE DATABASE tenant_enterprise_corp;
-- Подключение к базе данных арендатора
\c tenant_enterprise_corp
-- Создание таблиц (не требуется tenant_id!)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
Динамическое управление подключениями
// Менеджер пула подключений
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()
// Проверка после получения блокировки на запись
if db, exists := m.pools[tenantID]; exists {
return db, nil
}
// Создание нового подключения
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
}
Преимущества отдельных баз данных
- Максимальная изоляция: Полное разделение данных
- Лучшая безопасность: Нет риска доступа к данным других арендаторов
- Полная настраиваемость: Каждый арендатор может иметь полностью разные схемы
- Независимое масштабирование: Масштабирование баз данных арендаторов индивидуально
- Легкое соответствие требованиям: Соответствует самым строгим требованиям к изоляции данных
- Резервное копирование на арендатора: Простое, независимое восстановление
- Нет шумных соседей: Нагрузки арендаторов не влияют друг на друга
Недостатки отдельных баз данных
- Высокая стоимость: Множество экземпляров баз данных требуют больше ресурсов
- Операционная сложность: Управление многими базами данных (резервное копирование, мониторинг, миграции)
- Ограничения подключений: Каждый экземпляр базы данных имеет ограничения на подключения
- Аналитика между арендаторами: Требуется федерация данных или ETL
- Сложность миграции: Необходимо выполнять миграции во всех базах данных
- Перерасход ресурсов: Больше памяти, CPU и хранилища требуется
Лучше всего подходит для
- Корпоративного SaaS с высокоценными клиентами
- Строгих требований к соответствию (HIPAA, GDPR, SOC 2)
- Когда арендаторам нужна значительная настраиваемость
- Низкое до среднее количество арендаторов (десятки до низких сотен)
- Когда у арендаторов очень разные модели данных
Рассмотрение вопросов безопасности
Независимо от выбранного шаблона, безопасность имеет первостепенное значение:
1. Рядно-уровневая безопасность (RLS)
RLS PostgreSQL автоматически фильтрует запросы по арендатору, обеспечивая уровень безопасности на уровне базы данных. Эта функция особенно мощна для многоарендных приложений. Для получения дополнительной информации о PostgreSQL RLS, политиках безопасности и других продвинутых функциях PostgreSQL, см. нашу Шпаргалку PostgreSQL.
-- Включение RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Политика для изоляции по арендатору
CREATE POLICY tenant_isolation ON orders
FOR ALL
USING (tenant_id = current_setting('app.current_tenant')::INTEGER);
-- Приложение устанавливает контекст арендатора
SET app.current_tenant = '123';
2. Фильтрация на уровне приложения
Всегда фильтруйте по tenant_id в коде приложения. Примеры ниже используют GORM, но разные ORM имеют свои собственные подходы к построению запросов. Для рекомендаций по выбору подходящего ORM для вашего многоарендного приложения, см. наше сравнение Go ORM.
// ❌ ПЛОХО - Отсутствует фильтр арендатора
db.Where("email = ?", email).First(&user)
// ✅ ХОРОШО - Всегда включайте фильтр арендатора
db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user)
// ✅ ЛУЧШЕ - Используйте области видимости или middleware
db.Scopes(TenantScope(tenantID)).Where("email = ?", email).First(&user)
3. Пулирование подключений
Используйте пулировщики подключений, поддерживающие контекст арендатора:
// PgBouncer с пулированием транзакций
// Или используйте маршрутизацию подключений на уровне приложения
4. Журналирование аудита
Отслеживайте весь доступ к данным арендатора:
type AuditLog struct {
ID uint
TenantID uint
UserID uint
Action string
Table string
RecordID uint
Timestamp time.Time
IPAddress string
}
Оптимизация производительности
Стратегия индексирования
Правильное индексирование имеет решающее значение для производительности многоарендной базы данных. Понимание стратегий индексирования SQL, включая составные и частичные индексы, является обязательным. Для всестороннего справочника по командам SQL, включая CREATE INDEX и оптимизацию запросов, см. нашу Шпаргалку SQL. Для специфичных для PostgreSQL функций индексирования и настройки производительности, см. нашу Шпаргалку PostgreSQL.
-- Составные индексы для запросов арендаторов
CREATE INDEX idx_orders_tenant_created ON orders(tenant_id, created_at DESC);
CREATE INDEX idx_orders_tenant_status ON orders(tenant_id, status);
-- Частичные индексы для распространенных запросов, специфичных для арендатора
CREATE INDEX idx_orders_active_tenant ON orders(tenant_id, created_at)
WHERE status = 'active';
Оптимизация запросов
// Используйте подготовленные выражения для запросов арендаторов
stmt := db.Prepare("SELECT * FROM users WHERE tenant_id = $1 AND email = $2")
// Пакетные операции на арендатора
db.Where("tenant_id = ?", tenantID).Find(&users)
// Используйте пулирование подключений на арендатора (для шаблона отдельных баз данных)
Мониторинг
Эффективные инструменты управления базами данных необходимы для мониторинга многоарендных приложений. Вам нужно будет отслеживать производительность запросов, использование ресурсов и состояние базы данных для всех арендаторов. Для сравнения инструментов управления базами данных, которые могут помочь в этом, см. наше сравнение DBeaver vs Beekeeper. Оба инструмента предлагают отличные функции для управления и мониторинга баз данных PostgreSQL в многоарендных средах.
Мониторьте метрики на арендатора:
- Производительность запросов на арендатора
- Использование ресурсов на арендатора
- Количество подключений на арендатора
- Размер базы данных на арендатора
Стратегия миграции
Шаблон общей схемы
При реализации миграций баз данных ваш выбор ORM влияет на то, как вы обрабатываете изменения схемы. Примеры ниже используют функцию AutoMigrate GORM, но разные ORM имеют разные стратегии миграции. Для подробной информации о том, как различные Go ORM обрабатывают миграции и управление схемами, см. наше сравнение Go ORM.
// Миграции применяются ко всем арендаторам автоматически
func Migrate(db *gorm.DB) error {
return db.AutoMigrate(&User{}, &Order{}, &Product{})
}
Шаблон отдельной схемы/базы данных
// Миграции должны выполняться на каждом арендаторе
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
}
Матрица решений
| Фактор | Общая схема | Отдельная схема | Отдельная БД |
|---|---|---|---|
| Изоляция | Низкая | Средняя | Высокая |
| Стоимость | Низкая | Средняя | Высокая |
| Масштабируемость | Высокая | Средняя | Низкая-Средняя |
| Настраиваемость | Нет | Средняя | Высокая |
| Операционная сложность | Низкая | Средняя | Высокая |
| Соответствие требованиям | Ограниченное | Хорошее | Отличное |
| Лучшее количество арендаторов | 1000+ | 10-1000 | 1-100 |
Гибридный подход
Вы можете комбинировать шаблоны для разных уровней арендаторов:
// Малые арендаторы: Общая схема
if tenant.Tier == "standard" {
return GetSharedDB(tenant.ID)
}
// Корпоративные арендаторы: Отдельная база данных
if tenant.Tier == "enterprise" {
return GetTenantDB(tenant.ID)
}
Лучшие практики
- Всегда фильтруйте по арендатору: Никогда не доверяйте только коду приложения; используйте RLS, когда это возможно. Понимание основ SQL помогает обеспечить правильное построение запросов - обратитесь к нашей Шпаргалке SQL для лучших практик запросов.
- Мониторьте использование ресурсов арендаторами: Определяйте и ограничивайте “шумных соседей”. Используйте инструменты управления базами данных, такие как те, что сравниваются в нашем руководстве DBeaver vs Beekeeper, чтобы отслеживать метрики производительности.
- Реализуйте middleware для контекста арендатора: Централизуйте извлечение и проверку арендатора. Ваш выбор ORM влияет на то, как вы реализуете это - см. наше сравнение Go ORM для разных подходов.
- Используйте пулирование подключений: Эффективно управляйте подключениями к базе данных. Стратегии пулирования подключений, специфичные для PostgreSQL, рассматриваются в нашей Шпаргалке PostgreSQL.
- Планируйте миграцию арендаторов: Возможность перемещения арендаторов между шаблонами
- Реализуйте мягкое удаление: Используйте deleted_at вместо жесткого удаления данных арендатора
- Аудит всего: Журналируйте весь доступ к данным арендатора для соответствия требованиям
- Тестируйте изоляцию: Регулярные аудиты безопасности для предотвращения утечки данных между арендаторами
Заключение
Выбор правильного шаблона многокорпусной базы данных зависит от ваших конкретных требований к изоляции, стоимости, масштабируемости и операционной сложности. Шаблон Общая база данных, общая схема хорошо подходит для большинства SaaS-приложений, в то время как Отдельная база данных для каждого арендатора необходима для корпоративных клиентов с жесткими требованиями к соответствию нормам.
Начните с самого простого шаблона, который соответствует вашим требованиям, и планируйте миграцию на более изолированный шаблон по мере изменения ваших потребностей. Всегда приоритизируйте безопасность и изоляцию данных, независимо от выбранного шаблона.
Полезные ссылки
- Документация PostgreSQL по безопасности на уровне строк
- Архитектура базы данных для многокорпусных SaaS-приложений
- Проектирование многокорпусных баз данных
- Сравнение Go ORM для PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Шпаргалка PostgreSQL: быстрый справочник разработчика
- DBeaver vs Beekeeper - инструменты управления SQL-базами данных
- Шпаргалка по SQL - самые полезные команды SQL