赞
踩
在很多具体应用场景中,我们需要用到动态数据源的情况,比如多租户的场景,系统登录时需要根据用户信息切换到用户对应的数据库。又比如业务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(); } }
这里的 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; } }
源码分析发现数据源是通过environment.getDataSource()
方式获取的,第一时间我想到我们替换掉这个 environment
,后来发现不行,这个类牵扯的代码太多,不可能重写
又随即想到这是一个接口
public interface DataSource extends CommonDataSource, Wrapper {
Connection getConnection() throws SQLException;
Connection getConnection(String username, String password)
throws SQLException;
}
就是说不管怎么样,要进行数据库操作一定要调用实现这个接口的方法,那么我们可以替换掉 org.apache.ibatis.mapping.Environment
中的 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();
}
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(); } }
注入到spring 容器
@Configuration
public class DbConfig {
@Bean
public DyDataSource dataSource() {
return new DyDataSource(HikariDataSource.class);
}
}
此时调用dao层需要手动设置租户信息
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
TableTestServiceImpl testService;
@GetMapping("getById")
public TableTest getById(){
DyDataSource.setDbKey("uuu");
return testService.getById(1);
}
}
虽然代码实现了但是还有些隐患
第一个问题兴许可以通过redis这种做lru的过期淘汰策略,但是第二种目前还没找到方案,有想法的小伙伴可以评论下,让我学习下,谢谢!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。