这是我参与8月更文挑战的第11天,活动详情查看:8月更文挑战
需求
在很多具体应用场景中,我们需要用到动态数据源的情况,比如多租户的场景,系统登录时需要根据用户信息切换到用户对应的数据库。又比如业务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
即可实现我们的目的,上文分析这个类也是注入的,这样修改很方便,此时有两种方案:
- 自定义一个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();
}
复制代码
- 也是重写一个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();
}
}
复制代码
注入到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);
}
}
复制代码
总结
虽然代码实现了但是还有些隐患
- 目前的dataSource 只有新建没有回收,这样的话需要我们去写一个生命周期的维护,否则 dataSource 越来越多,但是可能需要用到的不足10%,因为dataSource 多了,其中的数据库连接也是一直存活的,极端情况下会占用大量的tcp连接
- 在初始化dataSource那块的代码虽然加了dcl但是也是有小概率出现线程问题(自行百度 dcl 缺陷)
第一个问题兴许可以通过redis这种做lru的过期淘汰策略,但是第二种目前还没找到方案,有想法的小伙伴可以评论下,让我学习下,谢谢!