一、多租户架构的那些事儿
咱们做系统开发的时候,经常会遇到一个需求:一套代码要服务多个客户,但数据得隔离开来。就像开酒店,每个客人住不同的房间,但共用大堂和电梯。在数据库领域,这就是典型的"多租户"需求。
SqlServer实现多租户主要有三种套路:
- 独立数据库:每个租户单独一个数据库,土豪专属方案
- 共享数据库+独立Schema:同一个数据库里用不同Schema隔离
- 共享表+租户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 分布式事务噩梦
跨租户统计报表时,如果采用独立数据库方案,分布式事务会让你怀疑人生。这时候可以:
- 使用ETL工具定时同步到数据仓库
- 采用最终一致性方案
-- 示例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"}');
五、性能优化三板斧
索引策略:在tenant_id字段上建立复合索引
CREATE INDEX idx_tenant_order ON orders(tenant_id, create_time);文件分组:不同租户分配到不同物理文件
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;资源调控:限制贪婪租户的资源占用
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';
七、终极选择指南
- 独立数据库:适合金融、医疗等隔离要求高的场景
- Schema隔离:SaaS应用的性价比之选
- 字段隔离:适合初创公司快速迭代
最后送大家一个万能决策树:
- 租户<10 → 独立数据库
- 租户10-100 → Schema隔离
- 租户>100 → 字段隔离+缓存策略
记住:没有最好的架构,只有最适合的架构。下次当产品经理说"我们就加个小功能"时,记得先问问他要支持多少个租户!
评论