Skip to content
28810 edited this page Mar 12, 2019 · 28 revisions

什么是多租户

维基百科:“软件多租户是指一种软件架构,在这种软件架构中,软件的一个实例运行在服务器上并且为多个租户服务”。一个租户是一组共享该软件实例特定权限的用户。有了多租户架构,软件应用被设计成为每个租户提供一个 专用的实例包括该实例的数据的共享,还可以共享配置,用户管理,租户自己的功能和非功能属性。多租户和多实例架构相比,多租户分离了代表不同的租户操作的多个实例。

多租户用于创建Saas(Software as-a service)应用(云处理)。

方案一:按租户字段区分

FreeSql.Repository 现实了 filter(过滤与验证)功能,如:

var topicRepos = fsql.GetGuidRepository<Topic>(t => t.TerantId == 1);

使用 topicRepos 对象进行 CURD 方法:

  • 在查询/修改/删除时附加此条件,从而达到不会修改 TerantId != 1 的数据;
  • 在添加时,使用表达式验证数据的合法性,若不合法则抛出异常;

利用这个功能,我们可以很方便的实现数据分区,达到租户的目的。

方案二:按租户分表

FreeSql.Repository 现实了 分表功能,如:

var tenantId = 1;
var reposTopic = orm.GetGuidRepository<Topic>(null, oldname => $"{oldname}{tenantId}");

上面我们得到一个仓储按租户分表,使用它 CURD 最终会操作 Topic_1 表。

更多说明参考:《FreeSql.Repository 仓储》

方案三:按租户分库

与方案二相同,只是表存储的位置不同,请查看 《FreeSql.Repository 仓储》《分表、分库》

多表查询

分表下的租户也支持多表查询,得益于 FreeSql 提供的优良基础。这部份仍然在 FreeSql.Repository 扩展库中实现的。

var tenantId = 1;
var reposTopic = orm.GetGuidRepository<Topic>(null, oldname => $"{oldname}{tenantId}");
var reposType = orm.GetGuidRepository<TopicType>(null, oldname => $"{oldname}{tenantId}");

//联表查询也支持
reposTopic.Select
    .FromRepository(reposType) //合并两个仓储的设置
    .LeftJoin<TopicType>((a, b) => a.TypeId == b.Id)
    .ToList();

上述代码的使用,将两个设置好的租户仓储合并起来查询,查询租户1下的 topic + topictype 数据,执行的 SQL语句:

SELECT ...
FROM "Topic_1" a 
LEFT JOIN "TopicType_1" b ON a."TypeId" = b."Id"

实现全局控制租户

FreeSql.Repository Autofac 注入方式实现了全局【过滤与验证】的设定,方便租户功能的设计;

public IServiceProvider ConfigureServices(IServiceCollection services) {

    services.AddSingleton<IFreeSql>(fsql);
    services.AddMvc();

    var builder = new ContainerBuilder();

    //示范全局过滤的仓储类注入,如果实体中不存在 Title 属性,则条件不生效
    builder.RegisterFreeRepository(filter => 
        filter.Apply<Song>("test", a => a.Title == DateTime.Now.ToString() + Thread.CurrentThread.ManagedThreadId)
    );

    builder.Populate(services);
    var container = builder.Build();
    return new AutofacServiceProvider(container);
}

public class xxxx {
    public int Id { get; set; }
}
public class Song {
    [Column(IsIdentity = true)]
    public int Id { get; set; }
    public string Title { get; set; }
}
//在控制器使用
public SongsController(GuidRepository<Song> repos1, GuidRepository<xxxx> repos2) {
}

第一次请求:

repos1.Select.ToSql()

"SELECT a."Id", a."Title" \r\nFROM "Song" a \r\nWHERE (a."Title" = strftime('%Y-%m-%d %H:%M.%f',datetime(current_timestamp,'localtime')) || 21)"

repos2.Select.ToSql()

"SELECT a."Id" \r\nFROM "xxxx" a"

第二次请求:

repos1.Select.ToSql()

"SELECT a."Id", a."Title" \r\nFROM "Song" a \r\nWHERE (a."Title" = strftime('%Y-%m-%d %H:%M.%f',datetime(current_timestamp,'localtime')) || 4)"

repos2.Select.ToSql()

"SELECT a."Id" \r\nFROM "xxxx" a"

//禁用过滤器 repos1.DataFilter.Disable("test")

repos1.Select.ToSql()

"SELECT a."Id", a."Title" \r\nFROM "Song" a"

1、注入的变量值在使用时有了动态变化,每次获取时都是新的(Thread.CurrentThread.ManagedThreadId);

2、设定的全局过滤,若某实体不存在表达式函数中的字段时,不会生效(如上xxxx不存在Title);

3、使用 DataFilter.Disable("test") 可临时关闭过滤器的效果,使用 DataFilter.Enable("test") 可重新开启;


## 参考资料

- [《读写分离》](https://github.com/2881099/FreeSql/wiki/%e8%af%bb%e5%86%99%e5%88%86%e7%a6%bb)
- [《性能》](https://github.com/2881099/FreeSql/wiki/%e6%80%a7%e8%83%bd)
- [《优化之:缓存之路》](https://github.com/2881099/FreeSql/wiki/%e7%bc%93%e5%ad%98)
- [《仓储层Repository》](https://github.com/2881099/FreeSql/wiki/Repository)
- [《学习FreeSql之一:添加数据》](https://github.com/2881099/FreeSql/wiki/%e6%b7%bb%e5%8a%a0)
- [《学习FreeSql之二:删除数据》](https://github.com/2881099/FreeSql/wiki/%e5%88%a0%e9%99%a4)
- [《学习FreeSql之三:修改数据》](https://github.com/2881099/FreeSql/wiki/%e4%bf%ae%e6%94%b9)
- [《优化之:延时加载》](https://github.com/2881099/FreeSql/wiki/%e5%bb%b6%e6%97%b6%e5%8a%a0%e8%bd%bd)
- [《优化之:贪婪加载》](https://github.com/2881099/FreeSql/wiki/%e8%b4%aa%e5%a9%aa%e5%8a%a0%e8%bd%bd)
Clone this wiki locally