随着云计算和SaaS模式的快速发展,多租户架构已经成为构建可扩展、高效且成本效益高的应用系统的关键。多租户架构允许单个应用实例同时为多个租户提供服务,每个租户都享有独立的数据、配置和隐私空间,同时共享相同的底层硬件和软件资源。在Java生态系统中,有多种方法和策略可以实现多租户系统。本文主要是关于java实现数据库层次的数据隔离相关代码实现。
基于数据库的隔离的三种方式
- 独立数据库模式:每个租户使用独立的数据库实例。这种方式提供了最高的数据隔离性和安全性,但也可能导致较高的硬件和管理成本。
- 独立模式:每个租户使用独立的数据库架构(schema)。这种方式在数据库实例层面实现了共享,但在架构层面保持了隔离,是一种折衷方案。
- 表字段模式:所有租户的数据都存储在同一个数据库和架构中,但每张表都有一个租户ID字段,对数据库操作都带上租户ID字段
1.独立数据库模式
不同的租户访问不同的数据源,建立自己的数据库连接池,我是通过dynamic来实现多数据源管理的,本质上就是一个map,存放key和数据库连接池信息。前端每次请求携带租户信息,到dynamic维护的map中查询是否存在,不存在则获取该租户的数据库连接信息创建连接池放入dynamic的map中。
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
setDataSource();
return true;
}
//设置本次访问数据源连接池
private void setDataSource() {
String dbCode = ServerSessionHolder.getSessionUser().getDbCode();
if (StringUtils.isNotBlank(dbCode)) {
DynamicRoutingDataSource dynamicRoutingDataSource = StaticMethodGetBean.applicationContext.getBean(DynamicRoutingDataSource.class);
if (!dynamicRoutingDataSource.getDataSources().containsKey(dbCode)) {
IUserClientService userClientService = StaticMethodGetBean.applicationContext.getBean(IUserClientService.class);
//获取登录租户的数据库连接信息
DataSourceInfoVO vo = userClientService.getByCode(dbCode);
dynamicRoutingDataSource.addDataSource(dbCode, CommonUtils.createDataSource(vo.getDbIp(), vo.getDbPort().toString(), vo.getDbUsername(), vo.getDbPasswd()));
}
DynamicDataSourceContextHolder.push(dbCode);
}
}
//创建数据库连接池
public static DruidDataSource createDataSource(String ip, String port, String userName, String passwd) {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(DatabaseTypeEnum.MYSQL.driverClass());
String url = DatabaseTypeEnum.MYSQL.jdbcPrefix().concat(ip).concat(COLON).concat(port).concat("?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai");
dataSource.setUrl(url);
dataSource.setUsername(userName);
dataSource.setPassword(passwd);
dataSource.setInitialSize(2);
dataSource.setMinIdle(2);
dataSource.setMaxActive(20);
return dataSource;
}
}
2.独立模式
不同租户使用同一个数据源的不同schema,这种方式比较节约资源,也方便数据库管理,每多一个租户就创建一个schema,创建相应的表结构。这种模式的重点是相同表名,不同的schema,我们需要采用不代码侵入的方式实现,这个我是通过mybatisplus实现的,mybatisplus中有个TableNameHandler接口,我们实现这个接口,实现它的dynamicTableName方法,就可以对sql语句中的表名进行处理。
public class DynamicTableNameHandler implements TableNameHandler {
@Override
public String dynamicTableName(String sql, String tableName) {
//获取本次访问租户对应的schema
String dbName = ServerSessionHolder.getSessionUser().getDbName();
if (!StringUtils.isEmpty(dbName)) {
tableName = dbName.concat(".").concat(tableName);
}
return tableName;
}
}
3.表字段模式
本种方式是每一张租户相关表中就加一个tenant_id字段,对数据库操作时每张表都带上这个字段信息,这种方式是隔离度最低的但也是最好管理的。这个是通过mybatisplus中的TenantLineHandler实现的。
public class MyTenantLineHandler implements TenantLineHandler {
//获取租户ID的值
@Override
public Expression getTenantId() {
return new LongValue(ServerSessionHolder.getSessionUser().getId());
}
//定义表中隔离字段名称,默认是tenant_id
@Override
public String getTenantIdColumn() {
return CommonConstant.CREATE_USER;
}
}
第二种和第三中方式需要加入MybatisPlusInterceptor中
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();
dynamicTableNameInnerInterceptor.setTableNameHandler(new DynamicTableNameHandler());
interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
interceptor.addInnerInterceptor(new MyTenantLineInnerInterceptor(new MyTenantLineHandler()));
return interceptor;
}
}
总结
在java中实现多租户需要结合实际情况选择相应的方案,没有最好的方案只有最适合的方案。通过合理的设计和选型,可以构建出既高效又安全的多租户系统。随着云计算和SaaS模式的不断发展,多租户系统的设计和实现将成为Java开发者的一项重要技能。