使用 Go 语言的多租户数据库模式示例

多租户数据库模式完整指南

目录

多租户 是 SaaS 应用程序的一种基本架构模式,允许多个客户(租户)共享相同的应用程序基础设施,同时保持数据隔离。

选择合适的数据库模式对于可扩展性、安全性和运营效率至关重要。

databases-scheme

多租户模式概述

在设计多租户应用程序时,您有三种主要的数据库架构模式可供选择:

  1. 共享数据库,共享模式(最常见)
  2. 共享数据库,独立模式
  3. 每个租户独立数据库

每种模式都有其独特的特点、权衡和使用场景。让我们逐一详细探讨。

模式 1:共享数据库,共享模式

这是最常见的多租户模式,所有租户共享同一个数据库和模式,使用 tenant_id 列来区分租户数据。

架构

┌─────────────────────────────────────┐
│     单个数据库                   │
│  ┌───────────────────────────────┐  │
│  │  共享模式                   │  │
│  │  - users (tenant_id, ...)    │  │
│  │  - orders (tenant_id, ...)   │  │
│  │  - products (tenant_id, ...) │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

实现示例

在实现多租户模式时,理解 SQL 基础知识至关重要。如需全面参考 SQL 命令和语法,请查看我们的 SQL 快速参考。以下是设置共享模式的示例:

-- 包含 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 快速参考

应用级过滤

在使用 Go 应用程序时,选择合适的 ORM 会显著影响多租户实现。以下示例使用 GORM,但还有其他几个优秀的选项。如需 Go ORM 的详细比较,包括 GORM、Ent、Bun 和 sqlc,请参阅我们的 PostgreSQL 的 Go ORM 综合指南

// 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
}

// 设置租户上下文的中间件
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 快速参考

-- 为租户创建模式
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 选项。如需 Go ORM 的详细比较,包括连接池、性能特征和使用场景,请参阅我们的 Go ORM 比较指南

// 带有模式搜索路径的连接字符串
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:每个租户独立数据库

每个租户获得自己的完整数据库实例,提供最大隔离性。

独立数据库架构

┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│  数据库 1    │  │  数据库 2    │  │  数据库 3    │
│  (租户 A)    │  │  (租户 B)    │  │  (租户 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)

PostgreSQL RLS 自动按租户过滤查询,提供数据库级别的安全层。此功能对多租户应用特别强大。如需更多关于 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 比较

// ❌ BAD - 缺少租户过滤
db.Where("email = ?", email).First(&user)

// ✅ GOOD - 始终包含租户过滤
db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user)

// ✅ BETTER - 使用作用域或中间件
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 命令和查询优化的全面参考,请参阅我们的 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 的选择会影响您如何处理模式更改。以下示例使用 GORM 的 AutoMigrate 功能,但不同 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)
}

最佳实践

  1. 始终按租户过滤:不要仅依赖应用程序代码;尽可能使用 RLS。理解 SQL 基础知识有助于确保正确的查询构建——请参阅我们的 SQL 快速参考 以了解查询最佳实践。
  2. 监控租户资源使用情况:识别并限制噪音邻居。使用我们 DBeaver vs Beekeeper 指南 中比较的数据库管理工具来跟踪性能指标。
  3. 实现租户上下文中间件:集中提取和验证租户。您的 ORM 选择会影响如何实现这一点——请参阅我们的 Go ORM 比较 以了解不同方法。
  4. 使用连接池:高效管理数据库连接。PostgreSQL 特定的连接池策略在我们的 PostgreSQL 快速参考 中有详细说明。
  5. 规划租户迁移:能够将租户迁移到不同模式
  6. 实现软删除:使用 deleted_at 而不是硬删除租户数据
  7. 记录所有内容:为合规性记录所有租户数据访问
  8. 测试隔离性:定期进行安全审计以防止跨租户数据泄露

结论

选择合适的多租户数据库模式取决于您对隔离性、成本、可扩展性和操作复杂性的具体要求。共享数据库、共享模式模式适用于大多数 SaaS 应用,而每个租户独立数据库模式对于有严格合规需求的企业客户是必要的。

从满足您需求的最简单模式开始,并随着需求的变化计划迁移到更隔离的模式。无论选择哪种模式,始终优先考虑安全性和数据隔离。

有用的链接