0


spring+Druid+mybatils多租户下动态切换数据源

什么是多租户

举个例子:蔡徐坤、展亚鹏和范小勤三个人去租房子,他们因为家里经济困难所以勤工俭学,三个人决定合租一套三室一厅的房子,虽然每个人有自己的房间,但是家里的水电、厨房、卫生间和热水器都是大家一起公用的。隐私性肯定是没有单独自己租房子来的高。

在多租户的架构里,多个租户共享相同的服务器、基础设施,数据库可以是共享的也可以是隔离的,由于多租户必定在用户规模上比单租户来的大,所以多租户一般会有多个实例,共用一套实例代码。租户之间的数据隔离往往采用逻辑隔离的方式,即在代码和数据库层面隔离,所以安全性远没有单租户来的高。

就比如上面举的例子,虽然三人都租有自己单独的房间,但房子里的的厨房、卫生间和洗衣机都是大家一起公用的。从方便和隐私的角度来看,都不如自己一个房子好。

在系统中,多租户体现为,多个租户共用一个或多个服务器、基础设施,数据库可以是共享也可以是隔离的,多个租户共用一套代码,或者在微服务中共用一个或者几个模块,租户和租户之间实现数据的隔离,但是安全性远不如单租户。但是其维护、修改成本都比单租户更低,因此如果系统是对安全性要求不这么高、定制性不这么强的系统,多租户是很好的一个方案。但对于一些大型网站、或者安全性需求强的网站,最好还是不用多租户。大厂的项目更多还是定制化开发,而中小厂为了节约成本可能会采用多租户。

多租户数据隔离的实现方式

基于字段隔离

在每一个表上都添加上租户id,所有数据都在一个库,查询时动态拼接租户id到sql。

优点:开发成本低,添加租户不需要做额外逻辑,跨租户逻辑简单

缺点:隔离程度最低,安全性最差,维护成本高,各租户数据耦合严重,维护成本高,每次的sql语句都需要拼接租户id,每个租户的数据量不能过大(可以后期分库分表)

基于表隔离

在表名上添加对应的租户信息,或使用视图进行数据过滤

优点:开发成本较低,隔离性相对较好,可以拥有相对较大的数据量

缺点:跨租户逻辑复杂,维护成本相对较高

基于库隔离

优点:隔离性最强,安全性最高,后期维护或者新增需求需要成本较小,灵活性更高

缺点:开发成本大,跨租户统计困难,新增租户时逻辑较复杂

基于mybatis实现基于库的数据分离

依赖:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-web</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.mybatis</groupId>
  7. <artifactId>mybatis</artifactId>
  8. <version>3.5.7</version>
  9. </dependency>
  10. <dependency>
  11. <groupId>mysql</groupId>
  12. <artifactId>mysql-connector-java</artifactId>
  13. <version>8.0.27</version> <!-- 根据你的实际情况选择版本号 -->
  14. </dependency>
  15. <dependency>
  16. <groupId>com.alibaba</groupId>
  17. <artifactId>druid-spring-boot-starter</artifactId>
  18. <version>1.2.6</version>
  19. </dependency>

实体类:

对应数据库表,存放数据库的基本信息和租户id,可以通过驱动类型实现不同数据源使用不同的数据库,例如a使用mysql,b使用postgresql

  1. package com.zy.saas.domian;
  2. import com.baomidou.mybatisplus.annotation.TableName;
  3. import lombok.Builder;
  4. import lombok.Data;
  5. import lombok.NoArgsConstructor;
  6. import lombok.experimental.Accessors;
  7. /**
  8. * @author: Larry
  9. * @Date: 2024 /01 /28 / 2:58
  10. * @Description:
  11. */
  12. @Data
  13. @Builder
  14. @Accessors(chain = true)
  15. @TableName("datasource")
  16. public class Datasource {
  17. private Integer id;
  18. /**
  19. * 数据库地址
  20. */
  21. private String url;
  22. /**
  23. * 数据库用户名
  24. */
  25. private String username;
  26. /**
  27. * 密码
  28. */
  29. private String password;
  30. /**
  31. * 数据库驱动
  32. */
  33. private String driverClassName;
  34. /**
  35. * 数据库key,即保存Map中的key
  36. */
  37. private String name;
  38. /**
  39. * 租户id
  40. */
  41. private Integer tenantId;
  42. }

核心类:

存放当前用户数据库url的线程变量

  1. package com.zy.saas.Context;
  2. /**
  3. * @author: Larry
  4. * @description:
  5. **/
  6. public class DataSourceContextHolder {
  7. //此类提供线程局部变量。这些变量不同于它们的正常对应关系是每个线程访问一个线程(通过get、set方法),有自己的独立初始化变量的副本。
  8. private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();
  9. /**
  10. * 设置数据源
  11. * @param dataSourceName 数据源名称
  12. */
  13. public static void setDataSource(String dataSourceName){
  14. DATASOURCE_HOLDER.set(dataSourceName);
  15. }
  16. /**
  17. * 获取当前线程的数据源
  18. * @return 数据源名称
  19. */
  20. public static String getDataSource(){
  21. return DATASOURCE_HOLDER.get();
  22. }
  23. /**
  24. * 删除当前数据源
  25. */
  26. public static void removeDataSource(){
  27. DATASOURCE_HOLDER.remove();
  28. }
  29. }

将主库的数据源信息根据yml导入,即默认数据源为yml里面配置的数据源

  1. package com.zy.saas.config;
  2. import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
  3. import com.zy.saas.DynamicDataSource;
  4. import org.springframework.boot.context.properties.ConfigurationProperties;
  5. import org.springframework.context.annotation.Bean;
  6. import org.springframework.context.annotation.Configuration;
  7. import org.springframework.context.annotation.Lazy;
  8. import org.springframework.context.annotation.Primary;
  9. import javax.sql.DataSource;
  10. import java.util.HashMap;
  11. import java.util.Map;
  12. /**
  13. * @author: Larry
  14. * @description: 设置数据源
  15. **/
  16. @Configuration
  17. public class DateSourceConfig {
  18. @Bean
  19. @ConfigurationProperties("spring.datasource.druid.master")
  20. public DataSource masterDataSource(){
  21. return DruidDataSourceBuilder.create().build();
  22. }
  23. @Bean(name = "dynamicDataSource")
  24. @Primary
  25. public DynamicDataSource createDynamicDataSource(){
  26. Map<Object,Object> dataSourceMap = new HashMap<>();
  27. DataSource defaultDataSource = masterDataSource();
  28. dataSourceMap.put("master",defaultDataSource);
  29. return new DynamicDataSource(defaultDataSource,dataSourceMap);
  30. }
  31. }

DynamicDataSource(动态数据源)是指在应用程序中根据需要动态切换数据源的机制。

通过这个类实现了对所有数据库信息的校验,保存。

  1. package com.zy.saas;
  2. import com.alibaba.druid.pool.DruidDataSource;
  3. import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
  4. import com.zy.saas.Context.DataSourceContextHolder;
  5. import com.zy.saas.domian.Datasource;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.springframework.beans.BeanUtils;
  8. import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
  9. import org.springframework.stereotype.Component;
  10. import javax.sql.DataSource;
  11. import java.sql.DriverManager;
  12. import java.sql.SQLException;
  13. import java.util.List;
  14. import java.util.Map;
  15. import java.util.Objects;
  16. /**
  17. * @author: Larry
  18. * @description: 实现动态数据源,根据AbstractRoutingDataSource路由到不同数据源中
  19. **/
  20. @Slf4j
  21. public class DynamicDataSource extends AbstractRoutingDataSource {
  22. private final Map<Object,Object> targetDataSourceMap;
  23. public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources){
  24. super.setDefaultTargetDataSource(defaultDataSource);
  25. super.setTargetDataSources(targetDataSources);
  26. this.targetDataSourceMap = targetDataSources;
  27. }
  28. @Override
  29. protected Object determineCurrentLookupKey() {
  30. return DataSourceContextHolder.getDataSource();
  31. }
  32. /**
  33. * 添加数据源信息
  34. *
  35. * @param dataSources 数据源实体集合
  36. */
  37. public void createDataSource(List<Datasource> dataSources){
  38. try {
  39. if (CollectionUtils.isNotEmpty(dataSources)){
  40. for (Datasource ds : dataSources) {
  41. //校验数据库是否可以连接
  42. Class.forName(ds.getDriverClassName());
  43. DriverManager.getConnection(ds.getUrl(),ds.getUsername(),ds.getPassword());
  44. //定义数据源
  45. DruidDataSource dataSource = new DruidDataSource();
  46. BeanUtils.copyProperties(ds,dataSource);
  47. //申请连接时执行validationQuery检测连接是否有效,这里建议配置为TRUE,防止取到的连接不可用
  48. dataSource.setTestOnBorrow(true);
  49. //建议配置为true,不影响性能,并且保证安全性。
  50. //申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
  51. dataSource.setTestWhileIdle(true);
  52. dataSource.init();
  53. this.targetDataSourceMap.put(ds.getName(),dataSource);
  54. }
  55. super.setTargetDataSources(this.targetDataSourceMap);
  56. // 将TargetDataSources中的连接信息放入resolvedDataSources管理
  57. super.afterPropertiesSet();
  58. }
  59. }catch (ClassNotFoundException | SQLException e) {
  60. log.error("---程序报错---:{}", e.getMessage());
  61. }
  62. }
  63. /**
  64. * 校验数据源是否存在
  65. * @param key 数据源保存的key
  66. * @return 返回结果,true:存在,false:不存在
  67. */
  68. public boolean existsDataSource(String key){
  69. return Objects.nonNull(this.targetDataSourceMap.get(key));
  70. }
  71. }

spring监听器,在spring后启动时自动触发一次,调用DynamicDataSource将数据源信息添加到

targetDataSourceMap里面

  1. package com.zy.saas.config;
  2. import com.zy.saas.DynamicDataSource;
  3. import com.zy.saas.Mapper.DataSourceMapper;
  4. import com.zy.saas.Service.UserService;
  5. import com.zy.saas.domian.Datasource;
  6. import com.zy.saas.util.JwtUtil;
  7. import lombok.extern.slf4j.Slf4j;
  8. import org.springframework.context.ApplicationListener;
  9. import org.springframework.context.event.ContextRefreshedEvent;
  10. import org.springframework.stereotype.Component;
  11. import org.springframework.util.CollectionUtils;
  12. import javax.annotation.Resource;
  13. import javax.sql.DataSource;
  14. import java.util.List;
  15. /**
  16. *
  17. * @author Larry
  18. */
  19. @Component
  20. @Slf4j
  21. public class ContentRefreshedEventListener implements ApplicationListener<ContextRefreshedEvent> {
  22. @Resource
  23. private DynamicDataSource dynamicDataSource;
  24. @Resource
  25. private DataSourceMapper dataSourceMapper;
  26. @Override
  27. public void onApplicationEvent(ContextRefreshedEvent event) {
  28. List<Datasource> dataourceList = dataSourceMapper.getListAll();
  29. System.out.println(dataourceList);
  30. if (!CollectionUtils.isEmpty(dataourceList)) {
  31. dynamicDataSource.createDataSource(dataourceList);
  32. }
  33. }
  34. }

主要思路

首先项目启动后,将所有租户的数据源信息通过Listener调用一次DynamicDataSource的createDataSource方法,将数据源信息存储到targetDataSourceMap里面,登陆时拦截器首先判断用户具体属于哪一个租户,获取租户id后,根据租户id判断出其所属的数据源,然后调用线程变量的set方法实现切换(默认数据源是在配置类里面配置的主库),当切换完成后调用remove防止不同线程变量出现访问错误。

注意事项

启动类上需要加上

  1. @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

否则会报循环依赖,如下

a4297472685d482ea051293c9cca450b.png

原因是

  1. org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

会引入一个Registrar注册了一个后置处理器,这个注册过程其实

  1. org.springframework.context.annotation.ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry

中完成的。这个后置处理器会在所有DataSource类型的Bean的初始化后进行处理,此时会去获取DataSourceInitializerInvoker类型的bean.而这个DataSourceInitializerInvoker类型的bean又会依赖DataSource,导致循环依赖。而这个bean其实作用是执行一些脚本的,可以不要,注册一个BeanDefinitionRegistryPostProcessor移除对应的后置处理器,这样在数据源初始化的时候就不会去获取DataSourceInitializerInvoker了。

这个当时卡了我很长时间

结语

本人能力有限,实现方式可能不是最优方法,希望有更好的方法的大佬可以在评论区提出来,大家发现我的错误或者有疑问的地方,可以在评论区@我

标签: mybatis sass

本文转载自: https://blog.csdn.net/zy11517/article/details/136343437
版权归原作者 不负朝阳 所有, 如有侵权,请联系我们删除。

“spring+Druid+mybatils多租户下动态切换数据源”的评论:

还没有评论