多租户
About 2182 wordsAbout 7 min
1984-01-24
多租户的例子均结合Spring Boot,可以 https://gitee.com/xiandafu/springboot3-beetl-beetlsql-example,根据使用场景,分位
- column-tenant-1: 单库单表多租户, 通过表的字段来区分多租户.采用beetlsql提供的sql重写功能
- column-tenant-2: 单库单表多租户, 通过表的字段来区分多租户.使用模板技术和自定义注解实现
- table-tenant-1:单库多表多租户,为每个租户建立一个表。采用beetlsql提供的sql重写功能,适合流行数据库(因为使用了jSqlParser支持主流数据库)
- table-tenant-2:单库多表租户,为每个租户建立一个表,使用beetl的模板技术
- schema-tenant: 单库多用户的多租户,使用数据库的用户来区分多租户,每个租户有一个数据用户,物理隔离
- database-tenant:多库租户,每个数据库一个租户,物理隔离,采用spring的动态数据源实现。这里的多库限于一种数据库
- database-style-tenant:同上,但数据库种类多样,每一类数据库都一个sqlmanager(sqlmananger的dbStyle设定,只能访问一种类型数据库)
单库单表多租户
采用表来实现多租户是最常见的情况,既每个租户通过tenant列的值来区别,如下表
Id | custeomer_name | tenant_id |
---|---|---|
1 | Xiaoli | 1 |
2 | Xiaoli | 2 |
实现单表租户,不一定需要特殊的设置,通常租户tenant字段在操作数据库前必传即可。也可以通过beetlsql的sql-rewrite
实现tenant字段自动插入到sql语句中
本例子来自 https://gitee.com/xiandafu/springboot3-beetl-beetlsql-example/tree/master/tenant/column-tenant-1
关于sql-rewrite
,请参考《BeetlSQL插件库》
column-tenant-1工程引入了如下pom
<dependency>
<groupId>com.ibeetl</groupId>
<artifactId>sql-rewrite</artifactId>
<version>${version}</version>
</dependency>
需要配置重写规则,当任何sql语句涉及的表包有tenant_id
字段的时候,发生sql重写,且重写的值是getCurrentValue
实现
@Bean
public SQLManagerCustomize mySQLManagerCustomize() {
return new SQLManagerCustomize() {
@Override
public void customize(String sqlMangerName, SQLManager manager) {
RewriteConfig rewriteConfig = new RewriteConfig();
rewriteConfig.config(manager);
rewriteConfig.addColRewriteConfig(new ColRewriteParam("tenant_id", new ColValueProvider() {
@Override
public Object getCurrentValue() {
return Db.localValue.get();
}
}));
}
};
}
关于sql重写,可以参考《BeetlSQL插件库》,这里举俩个简单例子
select * from order_log
-- 重写成
select * from order_log where tenant_id=?
insert into order_log (id) values (?)
-- 重写成
insert into order_log (id,tenant_id) values (?,?)
为了触发sql重写生效,默认下,需要DAO 从继承BaseMapper,改成RewriteBaseMapper,如下select方法将触发sql重写
@SqlResource("orderLog")
public interface MyRewriteMapper extends RewriteBaseMapper<OrderLog> {
List<OrderLog> select(String name);
@DisableRewrite /*可以使用此禁止sql重写*/
List<OrderLog> select2(String name);
}
@DisableRewrite是个特殊注解,此方法将不会产生SQL重写。
实现单表多租户,还可以不采用SQL重写,可以参考 https://gitee.com/xiandafu/springboot3-beetl-beetlsql-example/blob/master/tenant/column-tenant-2/ ,另外一个例子,提供tenant注解,每次显示在使用BeetlSQL时候,生成租户id。
究竟让Beetlsql自动重写sql,还是显示的在代码里设置租户id和显示的sql里添加租户字段,BeetlSQL作者无定论,个人偏好把这种单表多租户当着正常业务场景,使用后者。
单库多表多租户
也可以为每个租户使用一个表,比如对于1和2俩个租户,订单表如下
select * from order_log_1;
select * from order_log_2;
仍然跟其他多租户方案一样,通过sql重写实现或者是通过模板变量实现
SQL重写方案
例子参考 https://gitee.com/xiandafu/springboot3-beetl-beetlsql-example/tree/master/tenant/table-tenant-1
需要实现TableRewriteParam类,当sql语句中遇到order_log,对其表明进行更改,通过getTableName
实现
@Override
public void customize(String sqlMangerName, SQLManager manager) {
DBInitHelper.executeSqlScript(manager, "db/schema.sql");
RewriteConfig rewriteConfig = new RewriteConfig();
rewriteConfig.config(manager);
//所有sql表名有order_log,替换成order_log_${tenantId}
rewriteConfig.setTableRewriteConfig(new TableRewriteParam("order_log",new TableNameProvider(){
@Override
public String getTableName(String name) {
return name+"_"+ Db.localValue.get();
}
}));
}
模板变量方案
例子参考https://gitee.com/xiandafu/springboot3-beetl-beetlsql-example/tree/master/tenant/table-tenant-2
需要指定煮租户表为模板变量,比如
select * from ${toTable('order_log');
或者实体类定义
@Data
@Table(name="${toTable('order_log')}")
public class OrderLog {
@AutoID
Integer id;
String name;
}
toTable是你自己·定义的方法,用来返回真正的表名字,比如
BeetlTemplateEngine templateEngine = (BeetlTemplateEngine)manager.getSqlTemplateEngine();
//表${toTable('order_log')}是个虚拟的,数据库定义来自order_log
manager.addVirtualTable("order_log","${toTable('order_log')}");
// 注册一个方法来实现映射到多表的逻辑
templateEngine.getBeetl().getGroupTemplate().registerFunction("toTable", new Function(){
@Override
public Object call(Object[] paras, Context ctx) {
String tableName = (String)paras[0];
//租户表
return tableName+"_"+Db.localValue.get();
}
});
你也可以不使用模板方法,采用模板变量,
@Data
@Table(name="order_log_${tenantId}")
public class OrderLog {
@AutoID
Integer id;
String name;
}
这时候,需要提供此表的定义,以方便beetlsql知道OrderLog
对象对应的table实际上是order_log
,而不是order_log_${tenantId}
manager.addVirtualTable("order_log","order_log_${tenantId}");
单库多用户多租户
可以让每个租户使用一个数据库schema用户,比如租户1和租户2,在数据库中,有俩个用户user_1和user_2
select * from user_1.order_log;
select * from user_2.order_log;
实现多用户多租户,也跟单表多租户一样,有俩种办法
- 通过sql重写,为每个表字段增加前缀,比如order_log 变成 user_1.order_log
- 代码显示的指定用户,比如通过模板变量,访问
select * from ${tenantId}.order_log
, 根据上下文生成不同的租户sql语句
本节代码来自 https://gitee.com/xiandafu/springboot3-beetl-beetlsql-example/blob/master/tenant/schema-tenant/ 通过sql重写实现上诉第一种方式
@Bean
public SQLManagerCustomize schemaTenantSQLManagerCustomize() {
return new SQLManagerCustomize() {
@Override
public void customize(String sqlMangerName, SQLManager manager) {
//所有sql表名有order_log,替换成order_log_${tenantId}
rewriteConfig.setTableRewriteConfig(new SchemaRewriteParam(new TableNameProvider(){
@Override
public String getTableName(String name) {
//所有表都加上schema前缀,如果是其他数据库,可能name已经包含了默认shcema,比如public,需要替换
return "schema_"+Db.localValue.get()+"."+name;
}
}));
}
};
}
关于SQL重写,参考单表多租户或者直接参考《BeetlSQL插件库》
对于第二种方式,采用模板变量,可以通过BeetlSQL的SQLManagerExtend设置全局参数,这样,任何sql执行,都会包含tenantId参数
SQLManager.getSQLManagerExtend().setParaExtend(new ParaExtend(){
public Map morePara(ExecuteContext ctx){
Map map = new HashMap();
map.put("tenantId",Db.localValue.get())
}
}))
通过模板变量实现多租户还有更多细节,具体参考 https://gitee.com/xiandafu/springboot3-beetl-beetlsql-example/tree/master/tenant/table-tenant-2
多库多租户
有些情况,要为每个租户创建一个独立的数据源,这时候,可以利用Spring的多数据源技术,通过数据源切换,达到访问不同租户的的目的
需要注意,很多用户为了访问不同的数据业务库,也是用动态数据源,我觉得没有这种必要,可以使用多SQLManager来解决更直观,参考《多库使用》
多库多租户例子,参考 https://gitee.com/xiandafu/springboot3-beetl-beetlsql-example/tree/master/tenant/database-tenant,本节列出关键代码片段
首先需要定义切换租户,以联动切换数据源
public interface Routing {
void db(String dbName);
}
实现 AbstractRoutingDataSource,下面是database-tenant的例子中的代码片段
public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements Routing {
protected static ThreadLocal<String> dbThreadLocal = ThreadLocal.withInitial(() -> "");
//.....省略其他变量定义
@Override
public void db(String dbName) {
if (!checkExist(dbName)) {
throw new IllegalArgumentException("不存在的数据库 "+dbName);
}
dbThreadLocal.set(dbName);
}
//spring 自动调用,获取当前数据源
@Override
protected Object determineCurrentLookupKey() {
return dbThreadLocal.get();
}
//.....省略其他方法
}
因此,当租户调用的时候,需要先指定租户
@Service
public class OrderLogService {
@Autowired
MyCommonMapper myCommonMapper;
@Autowired
Routing routing;
public List<OrderLog> logs(String tenantId){
routing.db(tenantId);
return myCommonMapper.all();
}
}
多异构库多租户
BeetlSQL 针对每种数据库,都有不同dbStyle对应,以实现跨库的统一操作,因此,当租户初始化能选择数据库的种类,比如oracle,postgres,mysql等,使用多库多租户方案不在合适,因为他只针对一种数据库,必须采用本节异构库多租户方案
例子参考 https://gitee.com/xiandafu/springboot3-beetl-beetlsql-example/tree/master/tenant/database-style-tenant
此方案本质上是利用了Spring的动态数据源+多个SQLManager来完成,在多库多租户基础上做调整
public class DynamicRoutingDataSourceAndSqlManager extends DynamicRoutingDataSource {
@Override
public void db(String dbName) {
if (!checkExist(dbName)) {
throw new IllegalArgumentException(dbName);
}
//同时切换数据源和sqlManager
dbThreadLocal.set(dbName);
String dbType = dbType(dbName);
//每个数据库类型,如mysql,h2,oralce,对应一个sqlMaanger,参考配置 application-dynamic-ds-2.properties
ThreadLocalSQLManager.locals.set("sqlManager"+dbType);
}
protected String dbType(String dbName){
//实际使用应该有个缓存,能告诉dbname是用的是哪一种数据库(SQLManager)
if(dbName.startsWith("mysql")){
return "Mysql";
}else if(dbName.startsWith("h2")){
return "H2";
}else{
throw new UnsupportedOperationException(dbName);
}
}
}
这个db
方法既切换动态数据源,也通过Beetl的ThreadLocalSQLManager
切换到合适数据库类型的SQLManager。
BeetlSQL采用了SpringBoot 的 ThreadLocal配置方式,为每种数据库类型配置一个sqlManqger,如下例子,mysql和h2
beetlsql.sqlManagers = proxySqlManager
beetlsql.proxySqlManager.threadlocal=sqlManagerH2,sqlManagerMySql
beetlsql.proxySqlManager.basePackage=org.beetl.sql.springboot.mapper
beetlsql.sqlManagerH2.ds=h2Ds
beetlsql.sqlManagerH2.dbStyle=org.beetl.sql.springboot.dynamic.DynamicH2Style
beetlsql.sqlManagerMySql.ds=mysqlDs
beetlsql.sqlManagerMySql.dbStyle=org.beetl.sql.springboot.dynamic.DynamicMySqlStyle
使用例子同其他多租户方案一样,在访问数据库前,需要切换租户
@Service
public class OrderLogService {
@Autowired
OrderLogMapper myCommonMapper;
@Autowired
Routing routing;
public List<OrderLog> logs(String tenantId){
routing.db(tenantId);
return myCommonMapper.all();
}
}