引言
目录
- 引言
- 代码实现
- 1.动态数据源配置类Properties
- 2.数据源装配属性Bean
- 3.数据源工厂
- 4.创建动态数据源Holder
- 5.动态数据源配置类
- 6.添加动态数据源注解
- 7.添加注解切面
- 8.Demo运行
- 3.原理探究
首先,什么是动态数据源,网上其实已经有很多回答了。我个人的理解是: 能够在程序运行时根据不同的逻辑实现使用不同的数据源进行数据库操作
最常见的应用场景应该就是 多租户场景
、读写分离
。多租户通俗来讲就是A客户的数据需要用A库来存储,B客户的数据需要用B库来存储**,不同的租户之间的数据是隔离的,这样有利于管理和维护。当我们要去查询租户数据的方法时,就需要根据租户信息选择对应的数据库(Scheme)进行查询,这种方式就是动态数据源**。
本文将会利用Druid数据源实现动态的数据源,并且使用注解实现声明式数据源切换,最后根据源码利用反射实现动态数据源在程序运行时的热添加
代码实现
1.动态数据源配置类Properties
作用:主要用来获取配置文件中的数据源参数
@ConfigurationProperties(prefix ="dynamic")
public class CustomizedDynamicDatasourceProperties {
private Map<Object,CustomizedDynamicDatasourceBean> datasource = new HashMap();
public Map<Object, CustomizedDynamicDatasourceBean> getDatasource() {
return datasource;
}
public void setDatasource(Map<Object, CustomizedDynamicDatasourceBean> datasource) {
this.datasource = datasource;
}
}
同时,需要在application.yml
中添加数据源配置,如下
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xxxx/demo1?charset=utf8&serverTimezone=UTC
username: xxxxx
password: xxxxx
dynamic:
datasource:
slave1:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xxxx/demo1?charset=utf8&serverTimezone=UTC
username: xxxxx
password: xxxxx
slave2:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xxxx/demo2?charset=utf8&serverTimezone=UTC
username: xxxxx
password: xxxxx
slave3:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xxxxxxx/demo3?charset=utf8&serverTimezone=UTC
username: xxxxx
password: xxxxx
2.数据源装配属性Bean
作用:用来创建Druid数据源
@Data
public class CustomizedDynamicDatasourceBean {
/**
* 基本属性(必填)
*/
private String driverClassName;
private String url;
private String username;
private String password;
/**
* druid连接初始属性
*/
private int initialSize = 2;
private int maxActive = 10;
private int minIdle = 10;
private long maxWait = 60 * 1000L;
private long timeBetweenEvictionRunsMillis = 60 * 1000L;
private long minEvictableIdleTimeMillis = 1000L * 60L;
private long maxEvictableIdleTimeMillis = 1000L * 60L;
private boolean poolPreparedStatements = false;
private int maxOpenPreparedStatements = -1;
private boolean sharePreparedStatements = false;
private String filters = "stat,wall";
@Override
public String toString() {
return "CustomizedDynamicDatasourceBean{" +
"driverClassName='" + driverClassName + '\'' +
", url='" + url + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
", initialSize=" + initialSize +
", maxActive=" + maxActive +
", minIdle=" + minIdle +
", maxWait=" + maxWait +
", timeBetweenEvictionRunsMillis=" + timeBetweenEvictionRunsMillis +
", minEvictableIdleTimeMillis=" + minEvictableIdleTimeMillis +
", maxEvictableIdleTimeMillis=" + maxEvictableIdleTimeMillis +
", poolPreparedStatements=" + poolPreparedStatements +
", maxOpenPreparedStatements=" + maxOpenPreparedStatements +
", sharePreparedStatements=" + sharePreparedStatements +
", filters='" + filters + '\'' +
'}';
}
}
3.数据源工厂
作用:利用上面Bean属性创建数据源
public class CustomizedDynamicDatasourceFactory {
private final static Logger log = LoggerFactory.getLogger(CustomizedDynamicDatasourceFactory.class);
public static DruidDataSource build(CustomizedDynamicDatasourceBean properties) {
DruidDataSource druidDataSource = null;
try{
druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(properties.getDriverClassName());
druidDataSource.setUrl(properties.getUrl());
druidDataSource.setUsername(properties.getUsername());
druidDataSource.setPassword(properties.getPassword());
druidDataSource.setInitialSize(properties.getInitialSize());
druidDataSource.setMaxActive(properties.getMaxActive());
druidDataSource.setMinIdle(properties.getMinIdle());
druidDataSource.setMaxWait(properties.getMaxWait());
druidDataSource.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis());
druidDataSource.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis());
druidDataSource.setMaxEvictableIdleTimeMillis(properties.getMaxEvictableIdleTimeMillis());
druidDataSource.setPoolPreparedStatements(properties.isPoolPreparedStatements());
druidDataSource.setMaxOpenPreparedStatements(properties.getMaxOpenPreparedStatements());
druidDataSource.setSharePreparedStatements(properties.isSharePreparedStatements());
druidDataSource.setFilters(properties.getFilters());
} catch (Exception e) {
log.error("创建druid数据源时出现异常,{}",e.getMessage());
if(druidDataSource != null) {
druidDataSource.close();
}
}
return druidDataSource;
}
}
4.创建动态数据源Holder
作用:管理线程的数据源,可以进行数据源的切换
@Slf4j
public class CustomizedDynamicDatasourceHolder {
//后续使用(String : DruidDataSource)的方式存放当前线程的数据源,因此只需存放数据源的名称即可
private static final ThreadLocal<String> threadLocal = new ThreadLocal(){
@Override
protected Object initialValue() {
return "defaultDatasource";
}
};
/**
* 获取当前数据源
*/
public static String peek() {
return threadLocal.get();
}
/**
* 切换当前数据源为value
*/
public void shift(String value) {
//通过IOC容器获取bean
CustomizedDynamicDatasourceConfig bean = SpringUtil.get().getBean(CustomizedDynamicDatasourceConfig.class);
//判断数据源是否存在,不存在则抛出切换异常
Map<Object, DataSource> source = bean.getRegisteredDataSource();
if(StringUtils.isEmpty(name) || !source.containsKey(name)) {
throw new GlobalException("数据源" + name + "并未配置");
}
threadLocal.set(value);
log.info("数据源已切换为:{}",value);
}
/**
* 切换为默认数据源
*/
public void poll() {
threadLocal.set("defaultDatasource");
}
}
5.动态数据源配置类
作用:需要继承AbstractRoutingDataSource
并实现其方法
当配置了AbstractRoutingDataSource后,获取数据源时会根据其方法determineCurrentLookupKey()去获取数据源对应的key(在本文中就是数据源的名称),接着根据key找到对应的数据源并使用
@Configuration
@EnableConfigurationProperties(CustomizedDynamicDatasourceProperties.class)
@Slf4j
public class CustomizedDynamicDatasourceConfig extends AbstractRoutingDataSource {
@PostConstruct
public void setDefaultTargetDataSource() {
super.setDefaultTargetDataSource(getDefaultDataSource());
log.info("默认数据源注册成功 {}", getDefaultDataSource());
}
@PostConstruct
public void setTargetDataSources() {
super.setTargetDataSources(getTargetDataSource());
log.info("数据源 {} 注册成功", getTargetDataSource());
}
public Map<Object,DataSource> getRegisteredDataSource() {
return super.getResolvedDataSources();
}
@Autowired
private CustomizedDynamicDatasourceProperties properties;
private Map<Object, Object> getTargetDataSource() {
final HashMap<Object, Object> map = new HashMap<>();
properties.getDatasource().forEach(
(k, v) ->
{
//使用 工厂 + Bean 创建 DataSource
map.put(k, CustomizedDynamicDatasourceFactory.build(v));
}
);
return map;
}
@Override
protected Object determineCurrentLookupKey() {
return CustomizedDynamicDatasourceHolder.peek();
}
/**
* 默认数据源Bean对象
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public CustomizedDynamicDatasourceBean getDefaultDataSource() {
return new CustomizedDynamicDatasourceBean();
}
}
6.添加动态数据源注解
作用:能够通过声明式注解切换数据源
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@Inherited
public @interface Datasource {
// 数据源名称
String value();
}
7.添加注解切面
作用:实现声明式数据源切换
@Component
@Aspect
public class DatasourceAnnotationAop {
private static final Logger log = LoggerFactory.getLogger(DatasourceAnnotationAop.class);
@Pointcut("@annotation(xxx.xxxxxx.annotation.Datasource)")
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
//找到执行方法,再从方法上找注解
MethodSignature methodSignature = (MethodSignature)point.getSignature();
Method method = methodSignature.getMethod();
Datasource annotation = method.getAnnotation(Datasource.class);
String value = annotation.value();
if(!StringUtils.isEmpty(value)) {
DynamicDatasourceHolder.shift(value);
return point.proceed();
} else {
return point.proceed();
}
}
@After("pointcut()")
public void after() {
CustomizedDynamicDatasourceHolder.poll();
}
}
切换数据源
@Datasource("salve3")
8.Demo运行
数据库环境中demo1库中有A表,demo2库中有B表,demo3库中有C表,默认的数据源是Demo1,这里切换到demo3数据源并查询对应的C表数据
// 注解实现切换数据源
@Datasource("slave3")
public void multiDatasourceQuery() throws SQLException {
final String sql = "select * from C";
DataSource bean = SpringUtil.get().getBean(DataSource.class);
PreparedStatement statement = null;
try (Connection connection = bean.getConnection()) {
statement = connection.prepareStatement(sql);
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
assert statement != null;
statement.close();
}
}
测试结果
3.原理探究
为什么在
CustomizedDynamicDatasourceConfig
中继承类AbstractRoutingDataSource
并调用super.setTargetDataSources()
和super.setDefaultTargetDataSource()
方法就能够将数据源创建并能够使用determineCurrentLookupKey()
方法指定使用的数据源?
其实在类AbstractRoutingDataSource
就能发现,该类实现了InitializingBean
接口并实现了afterPropertiesSet()
方法
这就意味着当实例对象构造后会调用该方法,该方法实现如下
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
这里先需要知道AbstractRoutingDataSource
有几个属性
@Nullable
private Map<Object, Object> targetDataSources; //目标数据源集合,CustomizedDynamicDatasourceConfig中setTargetDatasources()方法就是为这个属性赋值
@Nullable
private Object defaultTargetDataSource; //默认数据源(数据源名称String)
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
@Nullable
private Map<Object, DataSource> resolvedDataSources; //已创建的数据源集合
@Nullable
private DataSource resolvedDefaultDataSource; //默认数据源(Datasource对象)
所以我们可以看到afterPropertiesSet()
其实就是根据targetDataSources
这个属性去创建数据源并添加到resolvedDataSources
中
这个时候其实targetDataSources
就是有值的了,因为在CustomizedDynamicDatasourceConfig
类中由@PostConstruct
标记的两个方法的的执行顺序比afterPropertiesSet()
要快,而这两个方法中进行对targetDataSources
和defaultTargetDataSource
两个属性都进行了赋值操作。
这个时候所有定义的数据源就都已经通过setTargetDataSources()
方法创建后并注册到AbstractRoutingDataSource
里了,每次要使用都会调用determineCurrentLookupKey
根据key去已注册的数据源中找到指定的那个。
使用@Transactional开启声明式事务后,使用多数据源操作数据库会出现什么问题?
1.首先手动获取Spring管理的DataSource,再获取连接,接着定义预处理prepareStatement
后执行获取结果,流程正常
2.使用Mybatis中selectById()
方法获取对数据库进行查询,流程正常
接着在方法上都添加@Transactional
注解开启事务,重复上面步骤发现,第一种方式查询数据库仍然正常,第二种方式报了以下的错误
Table 'demo1.C' doesn't exist
由此可以看出,数据源并没有切换,依旧是application.yml
中配置的默认的数据源,这是为什么呢?
- 这里就跟Spring事务机制有关系了,我们都知道Spring的事务是基于aop的,会在
@Transactional
注解标记的方法执行时生成代理环绕增强方法,在方法执行之前就已经开启事务并将事务设置为setAutoCommit(false)
- 此时开启事务时由于还没有执行数据源的切换,因此该事务是通过Spring的默认数据源开启的,所以表C也就不存在了
- 要验证这个解释很简单,只需要在
@Datasource
对应的切面DatasourceAnnotationAop
上添加注解@Order(-1)
,将代理顺序移到@Transactional
之前,再运行程序就会发现此时数据源成功切换了。
市面上其实也有比较成熟的数据源切换的框架可以使用,其实现更加灵活、稳定、安全和成熟,本文仅作为记录我在学习这部分的内容和遇到的问题的思考,谢谢阅读!