1. 当SAAS遇见ABP框架

想象你在为不同企业开发办公系统,每家企业都需要独立的数据存储和个性化配置。这种场景就像在同一栋写字楼里为不同公司划分专属楼层——这就是典型的多租户架构需求。ABP框架就像这栋楼的智能管理系统,原生支持多租户功能,提供了从租户识别到资源隔离的完整解决方案。

ABP的TenantManagerICurrentTenant接口形成了租户体系的核心。比如在控制器中获取当前租户信息:

public class ProductController : Controller
{
    private readonly ICurrentTenant _currentTenant;

    public ProductController(ICurrentTenant currentTenant)
    {
        _currentTenant = currentTenant;
    }

    public IActionResult GetProducts()
    {
        var tenantId = _currentTenant.Id; // 获取当前租户ID
        var tenantName = _currentTenant.Name; // 获取租户名称
        // 后续业务逻辑...
    }
}

(技术栈:ASP.NET Core + Entity Framework Core)

2. 租户模型的设计艺术

ABP的Volo.Abp.MultiTenancy.Tenant类提供了基础租户模型,我们通常需要扩展自定义字段:

public class CustomTenant : Tenant
{
    public string IndustryType { get; set; } // 所属行业分类
    public DateTime SubscriptionEndDate { get; set; } // 订阅到期时间

    public CustomTenant(Guid id, string name) 
        : base(id, name)
    {
    }
}

// 在DbContext中配置
protected override void OnModelCreating(ModelBuilder builder)
{
    builder.Entity<CustomTenant>(b =>
    {
        b.Property(t => t.IndustryType)
         .HasMaxLength(50)
         .IsRequired();
        
        b.Property(t => t.SubscriptionEndDate)
         .HasDefaultValue(DateTime.Now.AddYears(1));
    });
}

这种扩展方式保留了基础字段(ID、名称等),同时满足业务定制需求。记得在DomainSharedModule中配置聚合根关系。

3. 资源隔离的三大策略

3.1 数据库级隔离(军火库模式)

每个租户拥有独立数据库,在appsettings.json中配置:

"TenantDatabaseSettings": {
  "DefaultConnection": "Server=.;Database=MasterDB;Trusted_Connection=True",
  "TenantConnectionFormat": "Server=.;Database={tenantName}_DB;Trusted_Connection=True"
}

// 数据库选择器实现
public class CustomTenantConnectionStringResolver : DefaultConnectionStringResolver
{
    public override async Task<string> ResolveAsync(string connectionStringName)
    {
        if (connectionStringName == "Default")
        {
            var currentTenant = _currentTenantAccessor.Current;
            if (currentTenant != null)
            {
                return string.Format(
                    _configuration["TenantDatabaseSettings:TenantConnectionFormat"],
                    currentTenant.Name
                );
            }
        }
        return await base.ResolveAsync(connectionStringName);
    }
}

3.2 Schema模式隔离(小区分户)

共享数据库但使用不同Schema:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>(entity =>
    {
        entity.ToTable("Products", AbpSession.TenantId?.ToString("D"));
    });
}

// 动态Schema切换示例
using (_currentTenant.Change(tenantId))
{
    var products = await _productRepository.GetListAsync();
}

3.3 数据过滤(公寓合租)

使用EF Core的全局查询过滤器:

modelBuilder.Entity<Product>(entity =>
{
    entity.HasQueryFilter(p => p.TenantId == _currentTenant.Id);
});

// 临时禁用过滤器
var allData = await _productRepository
    .IgnoreQueryFilters()
    .ToListAsync();

4. 完整示例:模块化计费系统

我们实现一个跨租户的计费统计服务:

[UnitOfWork]
public class BillingService : ApplicationService
{
    private readonly IRepository<Product> _productRepository;

    public async Task<decimal> CalculateCrossTenantRevenue(List<Guid> tenantIds)
    {
        var totalRevenue = 0m;
        
        foreach (var tenantId in tenantIds)
        {
            using (_currentTenant.Change(tenantId))
            {
                var products = await _productRepository
                    .GetListAsync(p => p.IsActive);
                
                totalRevenue += products.Sum(p => p.Price * p.Stock);
            }
        }
        
        return totalRevenue;
    }
}

// 租户切换时自动处理:
// 1. 数据库连接切换
// 2. 缓存隔离
// 3. 设置当前用户上下文

5. 典型应用场景分析

某教育SAAS平台需要支持:

  • 培训机构独立管理学员数据
  • 品牌化定制登录页
  • 按模块订阅功能
  • 跨机构联合运营活动

采用混合隔离策略:核心业务数据使用独立数据库,公共配置数据采用Schema隔离,日志等次要数据使用租户ID过滤。ABP的模块化系统完美支持功能模块的按需加载。

6. 技术方案优劣势对比

优势

  1. 多级隔离策略组合应对复杂场景
  2. 开箱即用的租户生命周期管理
  3. 声明式的服务容器注册([ExposeServices]
  4. 动态菜单/权限系统适配多租户

挑战

  1. 跨租户事务需要特殊处理(使用Outbox模式)
  2. 大规模租户的数据库运维复杂度
  3. 租户个性化配置的存储方案选择
  4. 缓存雪崩风险的预防措施

7. 关键注意事项

  1. 连接池优化:当使用数据库级隔离时,连接字符串模板应包含Pooling=false参数避免资源泄露
  2. 跨租户查询防范:始终在仓储方法中显式包含TenantId过滤条件
  3. 后台作业处理:使用IBackgroundJobManager时需要手动设置租户上下文
  4. 性能监控重点:重点关注数据库连接切换耗时和EF Core查询计划重用率

8. 扩展技术点解析

Dapper与EF Core混合使用时的租户处理:

public async Task<List<Product>> GetProductsWithDapper()
{
    using (var connection = new SqlConnection(_connectionResolver.Resolve()))
    {
        return await connection.QueryAsync<Product>(
            "SELECT * FROM Products WHERE TenantId = @tenantId",
            new { tenantId = _currentTenant.Id }
        );
    }
}

9. 实施经验总结

经过多个SAAS项目的实战验证,建议采用这样的演进路线:

  1. 初期使用单数据库+租户ID过滤
  2. 规模增长后转向Schema隔离
  3. 最终按客户等级实施分库策略

定期使用ABP的DynamicProxy机制审核服务层的租户感知情况,结合SonarQube静态扫描确保没有遗漏过滤条件。记住:好的隔离设计就像保险机制——平时可能感受不到存在,但关键时刻绝对不可或缺。