一、多租户架构的那些事儿

咱们做系统开发的时候,经常会遇到一个需求:一套代码要服务多个客户,但数据得隔离开来。就像开酒店,每个客人住不同的房间,但共用大堂和电梯。在数据库领域,这就是典型的"多租户"需求。

SqlServer实现多租户主要有三种套路:

  1. 独立数据库:每个租户单独一个数据库,土豪专属方案
  2. 共享数据库+独立Schema:同一个数据库里用不同Schema隔离
  3. 共享表+租户ID字段:所有数据混在一起,靠字段区分
-- 示例1:创建租户专属Schema(SqlServer技术栈)
CREATE SCHEMA tenant_001;  -- 为001号租户创建独立命名空间
GO

CREATE TABLE tenant_001.orders (
    id INT PRIMARY KEY,
    order_no VARCHAR(50),
    -- 其他公共字段...
    tenant_id INT NOT NULL DEFAULT 001  -- 虽然放在独立schema,仍建议保留租户标记
);

二、方案选型的灵魂拷问

2.1 独立数据库方案

适合不差钱的主儿,每个租户都有自己完整的数据库实例。恢复数据的时候特别爽,直接还原单个库就行。但服务器开销大,维护成本高。

-- 示例2:动态创建租户数据库(SqlServer技术栈)
DECLARE @tenant_db NVARCHAR(100) = 'TenantDB_002';
DECLARE @sql NVARCHAR(MAX) = N'
CREATE DATABASE ' + QUOTENAME(@tenant_db) + '
ON PRIMARY (NAME=' + @tenant_db + '_data, 
            FILENAME=''C:\data\' + @tenant_db + '.mdf'')
LOG ON (NAME=' + @tenant_db + '_log,
        FILENAME=''C:\data\' + @tenant_db + '.ldf'')';

EXEC sp_executesql @sql;  -- 动态执行创建语句

2.2 共享数据库+独立Schema

性价比之选,DBA的最爱。一个数据库实例服务多个租户,通过Schema实现逻辑隔离。但要注意避免跨Schema查询拖慢性能。

-- 示例3:跨租户数据迁移(SqlServer技术栈)
BEGIN TRANSACTION;
-- 将5号租户的订单迁移到归档schema
INSERT INTO archive.orders_2023
SELECT * FROM tenant_005.orders 
WHERE create_time < '2023-01-01';

-- 迁移后删除原数据
DELETE FROM tenant_005.orders
WHERE create_time < '2023-01-01';
COMMIT TRANSACTION;

三、字段隔离的骚操作

最省资源的方案,所有租户数据都存在同一张表里,靠tenant_id字段区分。适合租户数量多但数据量小的场景。

-- 示例4:带租户过滤的视图(SqlServer技术栈)
CREATE VIEW current_tenant_orders AS
SELECT * FROM all_orders 
WHERE tenant_id = CONVERT(INT, SESSION_CONTEXT(N'tenant_id'));
-- 使用SESSION_CONTEXT存储当前租户信息

-- 设置当前租户(需要在连接时执行)
EXEC sp_set_session_context 'tenant_id', '003';

四、那些年我们踩过的坑

4.1 连接池污染

如果用字段隔离方案,一定要确保每次查询都带上tenant_id条件。建议用触发器兜底:

-- 示例5:防漏网之鱼的触发器(SqlServer技术栈)
CREATE TRIGGER tr_tenant_check
ON dbo.sensitive_data
INSTEAD OF INSERT, UPDATE, DELETE
AS
BEGIN
    DECLARE @current_tenant INT = CONVERT(INT, SESSION_CONTEXT(N'tenant_id'));
    
    -- 验证操作数据的租户归属
    IF EXISTS (
        SELECT 1 FROM inserted 
        WHERE tenant_id <> @current_tenant
    )
    BEGIN
        RAISERROR('跨租户数据操作被拒绝', 16, 1);
        ROLLBACK TRANSACTION;
        RETURN;
    END;
    
    -- 实际执行操作
    IF EXISTS (SELECT 1 FROM inserted)
        INSERT INTO dbo.sensitive_data
        SELECT * FROM inserted;
END;

4.2 分布式事务噩梦

跨租户统计报表时,如果采用独立数据库方案,分布式事务会让你怀疑人生。这时候可以:

  1. 使用ETL工具定时同步到数据仓库
  2. 采用最终一致性方案
-- 示例6:通过Service Broker实现异步通信(SqlServer技术栈)
-- 配置消息队列
CREATE QUEUE TenantDataQueue;
CREATE SERVICE TenantDataService ON QUEUE TenantDataQueue;

-- 发送跨租户消息
BEGIN DIALOG @dialog_handle
FROM SERVICE [InitiatorService]
TO SERVICE 'TenantDataService'
WITH ENCRYPTION = OFF;

SEND ON CONVERSATION @dialog_handle
MESSAGE TYPE [//sample/messages/tenant] ('{"tenant_id":5,"action":"sync"}');

五、性能优化三板斧

  1. 索引策略:在tenant_id字段上建立复合索引

    CREATE INDEX idx_tenant_order ON orders(tenant_id, create_time);
    
  2. 文件分组:不同租户分配到不同物理文件

    ALTER DATABASE MultiTenantDB 
    ADD FILEGROUP tenant_004_fg;
    
    ALTER DATABASE MultiTenantDB 
    ADD FILE (NAME = 'tenant_004_data', 
             FILENAME = 'C:\data\tenant_004.ndf')
    TO FILEGROUP tenant_004_fg;
    
  3. 资源调控:限制贪婪租户的资源占用

    CREATE WORKLOAD GROUP tenant_group_004
    USING "default";
    
    CREATE RESOURCE POOL tenant_pool_004
    WITH (MAX_CPU_PERCENT = 30);
    
    ALTER WORKLOAD GROUP tenant_group_004
    USING tenant_pool_004;
    

六、新潮玩法:弹性数据库

Azure SQL Database的弹性数据库功能简直是为多租户量身定制:

-- 示例7:创建分片映射(SqlServer技术栈)
-- 在分片映射管理器数据库中执行
CREATE SHARDING SCHEME tenant_sharding
AS RANGE LEFT FOR VALUES (100, 200, 300);

CREATE SHARD MAP MANAGER tenant_shard_manager;
GO

-- 添加分片数据库
EXEC tenant_shard_manager.sp_add_shard 
    @server_name = 'tenantdb_server1',
    @database_name = 'tenant_db_001';

七、终极选择指南

  1. 独立数据库:适合金融、医疗等隔离要求高的场景
  2. Schema隔离:SaaS应用的性价比之选
  3. 字段隔离:适合初创公司快速迭代

最后送大家一个万能决策树:

  • 租户<10 → 独立数据库
  • 租户10-100 → Schema隔离
  • 租户>100 → 字段隔离+缓存策略

记住:没有最好的架构,只有最适合的架构。下次当产品经理说"我们就加个小功能"时,记得先问问他要支持多少个租户!