一、动态数据源切换
再谈到动态数据源切换之前,我们先看一下常规的单数据库连接池(数据源)的配置过程,我们以Hikari连接池(Hikari官网-高效连接池)和Mybatis框架为例说明。
①Spring Mybatis Hikari datasource 配置
@Bean(name = "userReadHikariConfig")
public HikariConfig userReadHikariConfig() {
HikariConfig conf = new HikariConfig();
conf.setJdbcUrl(readUrl);
conf.setUsername(userName);
conf.setPassword(password);
conf.setPoolName("user-read-datasource");
conf.setConnectionTimeout(connectTimeout);
conf.setValidationTimeout(validateTimeout);
conf.setMaximumPoolSize(maximumPoolSize);
return conf;
}
@Bean(name = "userReadDataSource")
public DataSource userReadDataSource() {
return new HikariDataSource(userReadHikariConfig());
}
@Bean(name = "userReadSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(userReadDataSource());
sqlSessionFactoryBean.getObject().getConfiguration().setMapUnderscoreToCamelCase(true);
return sqlSessionFactoryBean.getObject();
}
如上所示,我们配置了一个用户数据库读库的连接池(一些变量赋值缺省),并构造注入了SqlSessionFactory。
SqlSessionFactory是MyBatis的关键对象,它是个单个数据库映射关系经过编译后的内存镜像。SqlSessionFactory一旦被创建,应该在应用执行期间都存在,它是创建SqlSession的工厂。
SqlSession也是MyBatis的关键对象,是执行持久化操作的独享,类似于JDBC中的Connection。它完全包含以数据库为背景的所有执行SQL操作的方法,它的底层封装了JDBC连接,可以用SqlSession实例来直接执行被映射的SQL语句。每个线程都应该有它自己的SqlSession实例,SqlSession的实例不能被共享,同时SqlSession也是线程不安全的。
②AbstractRoutingDatasource
单个数据源配置清楚了,多个数据源只需要重复上述配置即可,如果我们需要在业务读写分离,我们如何做到动态切换数据源呢,Spring为我们提供的抽象类AbstractRoutingDatasource实现了DataSource接口的用于获取数据库连接的方法,通过AOP的方式在程序运行时动态切换数据源。
具体来说怎么实现呢,首先编写AbstractRoutingDataSource的实现类DynamicDataSource,根据当前线程来选择数据源,然后通过AOP拦截特定的注解,设置当前的数据源信息。
public class DynamicDatasource extends AbstractRoutingDataSource {
private static class DynamicDataSourceHolder {
//使用ThreadLocal来保存当前线程需要使用的 dataSource 的 key。保证线程安全
private static final ThreadLocal<String> holder = new ThreadLocal<>();
private static void setDataSourceKey(String key) {
holder.set(key);
}
private static String getDataSourceKey() {
String key = holder.get();
if (null == key) {
key = DataSourceTypeEnum.READ.getName(); //默认写库
}
return key;
}
}
public static void setWrite() {
DynamicDataSourceHolder.setDataSourceKey(DataSourceTypeEnum.WRITE.getName());
}
public static void setRead() {
DynamicDataSourceHolder.setDataSourceKey(DataSourceTypeEnum.READ.getName());
}
@Override
protected Object determineCurrentLookupKey() {
// 一个读库实现。多个读库时可以在这里做负载均衡
return DynamicDataSourceHolder.getDataSourceKey();
}
//这个函数看似没用,在后面分布式事务的时候再说明
public String getCurrentDatasourceKey()
{
return DynamicDataSourceHolder.getDataSourceKey();
}
public enum DataSourceTypeEnum {
WRITE("write"),
READ("read");
private String name;
DataSourceTypeEnum(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
}
以上核心是重写determineCurrentLookupKey这个方法,可以看到它的实现是通过getDataSourceKey获取threadlocal里保存的key,而在AbstractRoutingDataSource 里通过key和datasource的map,找到对应的datasource。感兴趣的同学可以看一下AbstractRoutingDataSource这个类的源码,这里不在粘贴详述。
DynamicDataSource编写完成之后,我们用它来替换①中SqlSessionFactory构造是直接使用的HikariDatasource,具体代码如下:
@Bean(name = "userReadHikariConfig")
public HikariConfig userReadHi