举个例子:蔡徐坤、展亚鹏和范小勤三个人去租房子,他们因为家里经济困难所以勤工俭学,三个人决定合租一套三室一厅的房子,虽然每个人有自己的房间,但是家里的水电、厨房、卫生间和热水器都是大家一起公用的。隐私性肯定是没有单独自己租房子来的高。
在多租户的架构里,多个租户共享相同的服务器、基础设施,数据库可以是共享的也可以是隔离的,由于多租户必定在用户规模上比单租户来的大,所以多租户一般会有多个实例,共用一套实例代码。租户之间的数据隔离往往采用逻辑隔离的方式,即在代码和数据库层面隔离,所以安全性远没有单租户来的高。
就比如上面举的例子,虽然三人都租有自己单独的房间,但房子里的的厨房、卫生间和洗衣机都是大家一起公用的。从方便和隐私的角度来看,都不如自己一个房子好。
在系统中,多租户体现为,多个租户共用一个或多个服务器、基础设施,数据库可以是共享也可以是隔离的,多个租户共用一套代码,或者在微服务中共用一个或者几个模块,租户和租户之间实现数据的隔离,但是安全性远不如单租户。但是其维护、修改成本都比单租户更低,因此如果系统是对安全性要求不这么高、定制性不这么强的系统,多租户是很好的一个方案。但对于一些大型网站、或者安全性需求强的网站,最好还是不用多租户。大厂的项目更多还是定制化开发,而中小厂为了节约成本可能会采用多租户。
在每一个表上都添加上租户id,所有数据都在一个库,查询时动态拼接租户id到sql。
优点:开发成本低,添加租户不需要做额外逻辑,跨租户逻辑简单
缺点:隔离程度最低,安全性最差,维护成本高,各租户数据耦合严重,维护成本高,每次的sql语句都需要拼接租户id,每个租户的数据量不能过大(可以后期分库分表)
在表名上添加对应的租户信息,或使用视图进行数据过滤
优点:开发成本较低,隔离性相对较好,可以拥有相对较大的数据量
缺点:跨租户逻辑复杂,维护成本相对较高
优点:隔离性最强,安全性最高,后期维护或者新增需求需要成本较小,灵活性更高
缺点:开发成本大,跨租户统计困难,新增租户时逻辑较复杂
org.springframework.boot spring-boot-starter-weborg.mybatis mybatis3.5.7 mysql mysql-connector-java8.0.27 com.alibaba druid-spring-boot-starter1.2.6
对应数据库表,存放数据库的基本信息和租户id,可以通过驱动类型实现不同数据源使用不同的数据库,例如a使用mysql,b使用postgresql
package com.zy.saas.domian; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; /** * @author: Larry * @Date: 2024 /01 /28 / 2:58 * @Description: */ @Data @Builder @Accessors(chain = true) @TableName("datasource") public class Datasource { private Integer id; /** * 数据库地址 */ private String url; /** * 数据库用户名 */ private String username; /** * 密码 */ private String password; /** * 数据库驱动 */ private String driverClassName; /** * 数据库key,即保存Map中的key */ private String name; /** * 租户id */ private Integer tenantId; }
存放当前用户数据库url的线程变量
package com.zy.saas.Context; /** * @author: Larry * @description: **/ public class DataSourceContextHolder { //此类提供线程局部变量。这些变量不同于它们的正常对应关系是每个线程访问一个线程(通过get、set方法),有自己的独立初始化变量的副本。 private static final ThreadLocalDATASOURCE_HOLDER = new ThreadLocal<>(); /** * 设置数据源 * @param dataSourceName 数据源名称 */ public static void setDataSource(String dataSourceName){ DATASOURCE_HOLDER.set(dataSourceName); } /** * 获取当前线程的数据源 * @return 数据源名称 */ public static String getDataSource(){ return DATASOURCE_HOLDER.get(); } /** * 删除当前数据源 */ public static void removeDataSource(){ DATASOURCE_HOLDER.remove(); } }
将主库的数据源信息根据yml导入,即默认数据源为yml里面配置的数据源
package com.zy.saas.config; import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; import com.zy.saas.DynamicDataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Primary; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * @author: Larry * @description: 设置数据源 **/ @Configuration public class DateSourceConfig { @Bean @ConfigurationProperties("spring.datasource.druid.master") public DataSource masterDataSource(){ return DruidDataSourceBuilder.create().build(); } @Bean(name = "dynamicDataSource") @Primary public DynamicDataSource createDynamicDataSource(){ Map
DynamicDataSource(动态数据源)是指在应用程序中根据需要动态切换数据源的机制。
通过这个类实现了对所有数据库信息的校验,保存。
package com.zy.saas; import com.alibaba.druid.pool.DruidDataSource; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import com.zy.saas.Context.DataSourceContextHolder; import com.zy.saas.domian.Datasource; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.sql.DriverManager; import java.sql.SQLException; import java.util.List; import java.util.Map; import java.util.Objects; /** * @author: Larry * @description: 实现动态数据源,根据AbstractRoutingDataSource路由到不同数据源中 **/ @Slf4j public class DynamicDataSource extends AbstractRoutingDataSource { private final Map
spring监听器,在spring后启动时自动触发一次,调用DynamicDataSource将数据源信息添加到
targetDataSourceMap里面
package com.zy.saas.config; import com.zy.saas.DynamicDataSource; import com.zy.saas.Mapper.DataSourceMapper; import com.zy.saas.Service.UserService; import com.zy.saas.domian.Datasource; import com.zy.saas.util.JwtUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import javax.annotation.Resource; import javax.sql.DataSource; import java.util.List; /** * * @author Larry */ @Component @Slf4j public class ContentRefreshedEventListener implements ApplicationListener{ @Resource private DynamicDataSource dynamicDataSource; @Resource private DataSourceMapper dataSourceMapper; @Override public void onApplicationEvent(ContextRefreshedEvent event) { List dataourceList = dataSourceMapper.getListAll(); System.out.println(dataourceList); if (!CollectionUtils.isEmpty(dataourceList)) { dynamicDataSource.createDataSource(dataourceList); } } }
首先项目启动后,将所有租户的数据源信息通过Listener调用一次DynamicDataSource的createDataSource方法,将数据源信息存储到targetDataSourceMap里面,登陆时拦截器首先判断用户具体属于哪一个租户,获取租户id后,根据租户id判断出其所属的数据源,然后调用线程变量的set方法实现切换(默认数据源是在配置类里面配置的主库),当切换完成后调用remove防止不同线程变量出现访问错误。
启动类上需要加上
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
否则会报循环依赖,如下
原因是org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration会引入一个Registrar注册了一个后置处理器,这个注册过程其实org.springframework.context.annotation.ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry中完成的。这个后置处理器会在所有DataSource类型的Bean的初始化后进行处理,此时会去获取DataSourceInitializerInvoker类型的bean.而这个DataSourceInitializerInvoker类型的bean又会依赖DataSource,导致循环依赖。而这个bean其实作用是执行一些脚本的,可以不要,注册一个BeanDefinitionRegistryPostProcessor移除对应的后置处理器,这样在数据源初始化的时候就不会去获取DataSourceInitializerInvoker了。
这个当时卡了我很长时间
本人能力有限,实现方式可能不是最优方法,希望有更好的方法的大佬可以在评论区提出来,大家发现我的错误或者有疑问的地方,可以在评论区@我