当前位置:   article > 正文

mybatis plus+spring boot 多租户动态数据源实现方案_mybatisplus 多租户独立数据源

mybatisplus 多租户独立数据源

需求

在很多具体应用场景中,我们需要用到动态数据源的情况,比如多租户的场景,系统登录时需要根据用户信息切换到用户对应的数据库。又比如业务A要访问A数据库,业务B要访问B数据库等,都可以使用动态数据源方案进行解决。
作为合格的程序员第一时间肯定是去百度,但是呢既然我写了这篇博客那么肯定是没能很好的集成到我项目中,网上写的一篇文章说基于spring的AbstractRoutingDataSource 就可以实现,但是我试了不行,因为我自己项目不是用的jdbcTemplate 也可能是我使用姿势不对,反正没有用上,但是也确实给了我灵感
首先我肯定要知道是通过什么方式去获取的db connect,看了源码心中有数

源码调试

首先mybatis plus也是集成的mybatis,那么最核心的一定是 SqlSessionFactory,
这是一个接口,项目中只有 DefaultSqlSessionFactory 实现了此接口
此时只要项目中打个断点就知道是在 org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      //关键就在于 environment.getDataSource()
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

这里的 dataSource 是在 org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration 注入的(别问为啥知道的,问就是百度的)
因为小编项目中用的就是spring默认的连接池,所以是通过如下代码注入

@ConditionalOnClass(HikariDataSource.class)
	@ConditionalOnMissingBean(DataSource.class)
	@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource", matchIfMissing = true)
	static class Hikari {

		@Bean
		@ConfigurationProperties(prefix = "spring.datasource.hikari")
		public HikariDataSource dataSource(DataSourceProperties properties) {
			HikariDataSource dataSource = createDataSource(properties,
					HikariDataSource.class);
			if (StringUtils.hasText(properties.getName())) {
				dataSource.setPoolName(properties.getName());
			}
			return dataSource;
		}

	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

思路

源码分析发现数据源是通过environment.getDataSource() 方式获取的,第一时间我想到我们替换掉这个 environment,后来发现不行,这个类牵扯的代码太多,不可能重写
又随即想到这是一个接口

public interface DataSource  extends CommonDataSource, Wrapper {

  Connection getConnection() throws SQLException;

  Connection getConnection(String username, String password)
    throws SQLException;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

就是说不管怎么样,要进行数据库操作一定要调用实现这个接口的方法,那么我们可以替换掉 org.apache.ibatis.mapping.Environment中的 dataSource即可实现我们的目的,上文分析这个类也是注入的,这样修改很方便,此时有两种方案:

  • 自定义一个dataSource,然后声明一个属性,通过租户的key去选择指定的dataSource,再调用 getConnection 方法
	 private static ConcurrentMap<String, DataSource> dataSourceConcurrentMap = new ConcurrentHashMap<>();
	 
	 @Override
  	 public Connection getConnection() throws SQLException {
   	String dbKey = threadLocal.get();
       DataSource dataSource = dataSourceConcurrentMap.get(dbKey);
       return getDataSource().getConnection();
   }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 也是重写一个dataSource,但是重写更彻底,相当于重写一个数据库连接池,也是类似操作,根据租户的信息创建指定的 connect,并且自己维护数据库连接的生命周期

虽然后者实现起来更加优雅和刺激,但是呢我还是选择前者
不管从实现难度还是速度来看都是前者更好(后者我还没这个能力)

代码实现

import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.core.NamedThreadLocal;
import org.springframework.util.StringUtils;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;


@Slf4j
public class DyDataSource extends HikariDataSource {
	// 全局的数据源类型
    Class<? extends DataSource> type;

	// 当前线程的租户信息
    private static ThreadLocal<String> threadLocal = new NamedThreadLocal<>("TARGET_DATA_SOURCE");

 	// 数据源容器
    private static ConcurrentMap<String, DataSource> dataSourceConcurrentMap = new ConcurrentHashMap<>();

    public DyDataSource(Class<? extends DataSource> type) {
        super();
        this.type = type;
        log.info("DyDataSource init ...........");
    }

    @Override
    public Connection getConnection() throws SQLException {

        return getDataSource().getConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return getDataSource().getConnection(username, password);
    }

    public static void setDbKey(String dbKey) {
        threadLocal.set(dbKey);
    }

	// 根据租户信息获取数据源
    public DataSource getDataSource(){

        String dbKey = threadLocal.get();
        if (StringUtils.isEmpty(dbKey)) {
            throw new RuntimeException("未指定 dbKey");
        }

       DataSource dataSource = dataSourceConcurrentMap.get(dbKey);
        if (dataSource == null) {
            // 初始化
            synchronized (dbKey.intern()) {
                if (dataSource == null) {
                    dataSource = initDataSource(dbKey);
                }
            }
        }

        if (dataSource == null) {
            throw new RuntimeException("dataSource is null");
        }

        return dataSource;

    }
	// 根据租户信息初始化
    private DataSource initDataSource(String dbKey){

        DataSourceProperties dataSourceProperties = new DataSourceProperties();
        dataSourceProperties.setDriverClassName("com.mysql.jdbc.Driver");
        if (dbKey.equals("inner")) {
            dataSourceProperties.setUrl("jdbc:mysql://inner.com:3307/crawler?zeroDateTimeBehavior=convertToNull&characterEncoding=utf-8&useUnicode=true&useSSL=false");
            dataSourceProperties.setUsername("root");
            dataSourceProperties.setPassword("123456");
        } else if (dbKey.equals("local")) {
            dataSourceProperties.setUrl("jdbc:mysql://inner.com:3307/more_db?zeroDateTimeBehavior=convertToNull&characterEncoding=utf-8&useUnicode=true&useSSL=false");
            dataSourceProperties.setUsername("root");
            dataSourceProperties.setPassword("123456");
        }else {
            return null;
        }

        // 从源码中抄的,大概就是绑定数据
        return dataSourceProperties.initializeDataSourceBuilder().type(type).build();
    }


}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94

注入到spring 容器

@Configuration
public class DbConfig {
    @Bean
    public DyDataSource dataSource() {
        return new DyDataSource(HikariDataSource.class);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

此时调用dao层需要手动设置租户信息


@RestController
@RequestMapping("/test")
public class TestController {
    @Autowired
    TableTestServiceImpl testService;

    @GetMapping("getById")
    public TableTest getById(){
        DyDataSource.setDbKey("uuu");
        return testService.getById(1);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

总结

虽然代码实现了但是还有些隐患

  • 目前的dataSource 只有新建没有回收,这样的话需要我们去写一个生命周期的维护,否则 dataSource 越来越多,但是可能需要用到的不足10%,因为dataSource 多了,其中的数据库连接也是一直存活的,极端情况下会占用大量的tcp连接
  • 在初始化dataSource那块的代码虽然加了dcl但是也是有小概率出现线程问题(自行百度 dcl 缺陷)

第一个问题兴许可以通过redis这种做lru的过期淘汰策略,但是第二种目前还没找到方案,有想法的小伙伴可以评论下,让我学习下,谢谢!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/407633
推荐阅读
相关标签
  

闽ICP备14008679号