序言
最近有一个基于C/S(客户/服务端)结构的多版本内容控制的需求。服务端为所有客户端提供知识库内容支持,客户端可以通过前台页面在线更新服务端的知识库内容,客户端需要使用知识库的内容进行一系列的操作,具体做什么操作不是重点。
现在问题是产品需要支持在旧版本的知识库创建的作业更新后任然使用旧版本知识库内容,而创建新作业时使用更新的知识库内容
经过需求分析之后决定使用动态数据源来处理不同作业访问不同知识库内容的方式从而解决这个问题。
数据源连接
在 Spring
框架中,我们通常使用 DataSource
对象来管理数据库连接。javax.sql.DataSource
是一个 Java
接口,它提供了与数据库连接相关的方法和功能。它是连接数据库的一种方式,可以通过这个接口来获取数据库连接,执行 SQL
查询和更新等操作。
传统的 JDBC
访问数据库技术,每次访问数据库都需要通过数据库驱动器 Driver
和数据库名称以及密码等等资源建立数据库连接。频繁的建立数据库连接与断开数据库,这样会消耗大量的系统资源和时间,降低性能,需要一定的内存和 CPU 开销。
通过建立数据库连接池,将这些数据库连接保存在数据连接池中,访问数据库时,只需要从数据库连接池中获取空闲的数据库连接,当程序员访问数据库结束时,数据连接会放回数据库连接池中,从而大大提高系统性能。
SpringBoot官方提供的动态数据源
AbstractRoutingDataSource
是 springboot
官方提供给我们的多数据源切换策略,下面介绍一下该类中的核心属性和方法。
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
| public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable private Map<Object, Object> targetDataSources;
@Nullable private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
@Nullable private Map<Object, DataSource> resolvedDataSources;
@Nullable private DataSource resolvedDefaultDataSource;
@Override public void afterPropertiesSet() { if (this.targetDataSources == null) { throw new IllegalArgumentException("Property 'targetDataSources' is required"); } this.resolvedDataSources = CollectionUtils.newHashMap(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); } }
protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = this.resolvedDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } return dataSource; }
@Nullable protected abstract Object determineCurrentLookupKey();
|
所以使用 spring boot
官方提供的多数据源切换策略只需要实现 AbstractRoutingDataSource
类然后重写 determineCurrentLookupKey()
方法即可。
官方提供的多数据源切换策略相对较简单,还有一种第三方提供的动态数据源也是下面重点要介绍的。
苞米豆动态数据源
一般的实现方式是通过 ThreadLocal
和注解,通过 AOP
切面在需要切换数据源的地方设置需要切换的数据源的key即可,注意 ThreadLocal
设置使用完后需要清理,否则可能造成内存溢出。
这里主要介绍通过 baomidou
的 dynamic-datasource
来配置多数据源切换操作,自定义配置数据来源以及动态增减数据源。
由于项目中使用了 Druid
做数据库连接池,所以会一并介绍 Druid
和 dynamic-datasource
的整合方式。
依赖
1 2 3 4 5 6 7 8 9 10 11
| <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.14</version> </dependency>
<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>4.2.0</version> </dependency>
|
配置
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
| spring: datasource: druid: filter: stat: enabled: true log-slow-sql: true slow-sql-millis: 1000 merge-sql: false wall: config: multi-statement-allow: true webStatFilter: enabled: true statViewServlet: enabled: true allow: url-pattern: /druid/* login-username: admin login-password: 123456 dynamic: primary: master strict: false datasource: master: type: com.alibaba.druid.pool.DruidDataSource url: jdbc:postgresql://ip:port/database_1?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=Asia/Shanghai&reWriteBatchedInserts=true username: user_1 password: password driverClassName: org.postgresql.Driver
druid: initialSize: 5 minIdle: 10 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 maxEvictableIdleTimeMillis: 900000 validationQuery: SELECT version() testWhileIdle: true testOnBorrow: false testOnReturn: false
|
到这一步其实我们就可以通过 @DS ("配置的数据库名称")
注解来切换数据源了。
注解可以配置在类和方法上面,配置在方法上面的注解优先级大于配置在类上面的注解。
自定义数据源配置来源
有时候我们并不固定数据源的加载位置。例如在上述场景中,每次更新一次知识库都需要创建一个数据源连接,然后将这个连接存入 master
库,我们不可能每次新增一个连接就将数据源连接配置到yml
文件中,所以就需要系统启动时主动从别的地方获取数据源连接,这里我从 master
库获取。
我们先来看它的核心类 DynamicRoutingDataSource
的源码 ,除了 springboot
官方提供的多数据源切换操作之外,它还扩展提供了数据源分组、动态新增或删除数据源连接等方法。
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
| public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean { private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>(); private final Map<String, GroupDataSource> groupDataSources = new ConcurrentHashMap<>(); private final List<DynamicDataSourceProvider> providers; @Override public void afterPropertiesSet() { checkEnv(); Map<String, DataSource> dataSources = new HashMap<>(16); for (DynamicDataSourceProvider provider : providers) { Map<String, DataSource> dsMap = provider.loadDataSources(); if (dsMap != null) { dataSources.putAll(dsMap); } } for (Map.Entry<String, DataSource> dsItem : dataSources.entrySet()) { addDataSource(dsItem.getKey(), dsItem.getValue()); } if (groupDataSources.containsKey(primary)) { log.info("dynamic-datasource initial loaded [{}] datasource,primary group datasource named [{}]", dataSources.size(), primary); } else if (dataSourceMap.containsKey(primary)) { log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), primary); } else { log.warn("dynamic-datasource initial loaded [{}] datasource,Please add your primary datasource or check your configuration", dataSources.size()); } } }
|
注意 afterPropertiesSet()
方法中循环遍历了 providers
这个实例变量,并将loadDataSources()
方法返回的数据源添加到 dataSourceMap
中。
所以我们可以猜测 DynamicDataSourceProvider
是用来加载数据源连接的接口。
果然,不出我们所料。它有一个默认的实现类从 yml
配置文件中读取数据源连接。
这个默认的实现类中具体是如何创建数据源的不是我们关注的重点,我们主要看它是何时加载的,弄明白这一点那我们就可以自定义一个数据源加载类,加载我们所需要的数据源连接。
我们从引入的 starter
出发,在 META_INF/spring.factories
文件中可以看到自动配置类。
1 2
| org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration
|
点进去这个配置类,可以看到
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
| @Slf4j @Configuration @EnableConfigurationProperties(DynamicDataSourceProperties.class) @AutoConfigureBefore(value = DataSourceAutoConfiguration.class, name = "com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure") @Import({DruidDynamicDataSourceConfiguration.class, DynamicDataSourceCreatorAutoConfiguration.class, DynamicDataSourceAopConfiguration.class, DynamicDataSourceAssistConfiguration.class}) @ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) public class DynamicDataSourceAutoConfiguration implements InitializingBean {
private final DynamicDataSourceProperties properties;
private final List<DynamicDataSourcePropertiesCustomizer> dataSourcePropertiesCustomizers;
public DynamicDataSourceAutoConfiguration( DynamicDataSourceProperties properties, ObjectProvider<List<DynamicDataSourcePropertiesCustomizer>> dataSourcePropertiesCustomizers) { this.properties = properties; this.dataSourcePropertiesCustomizers = dataSourcePropertiesCustomizers.getIfAvailable(); }
@Bean @ConditionalOnMissingBean public DataSource dataSource(List<DynamicDataSourceProvider> providers) { DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource(providers); dataSource.setPrimary(properties.getPrimary()); dataSource.setStrict(properties.getStrict()); dataSource.setStrategy(properties.getStrategy()); dataSource.setP6spy(properties.getP6spy()); dataSource.setSeata(properties.getSeata()); dataSource.setGraceDestroy(properties.getGraceDestroy()); return dataSource; }
@Override public void afterPropertiesSet() { if (!CollectionUtils.isEmpty(dataSourcePropertiesCustomizers)) { for (DynamicDataSourcePropertiesCustomizer customizer : dataSourcePropertiesCustomizers) { customizer.customize(properties); } } }
}
|
可以看到初始化 dataSource
中传入了入参 providers
,那这个 providers
是从哪来的呢?我们可以发现配置类上面有一个 @Import
注解,引入了其他配置,进入最后一个 DynamicDataSourceAssistConfiguration
配置类,可以发现在这里注入了默认的 yml provider
所以我们也定义一个自动配置类,然后将我们自己的 provider
交给 spring
容器管理。
同时这里默认实现了一个抽象类 provider
供我们使用,我们不必去实现 DynamicDataSourceProvider
接口,只需要实现这个抽象类然后重写 executeStmt()
方法通过默认数据源执行 SQL
获取数据库中的连接即可。
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
| @Slf4j public class CustomDynamicDataSourceProvider extends AbstractJdbcDataSourceProvider {
private static final Pattern PATTERN = Pattern.compile("/([^/]+)\\?");
private final DataSourceProperty dataSourceProperty;
public CustomDynamicDataSourceProvider(DefaultDataSourceCreator defaultDataSourceCreator, DataSourceProperty dataSourceProperty) { super(defaultDataSourceCreator, dataSourceProperty.getDriverClassName(), dataSourceProperty.getUrl(), dataSourceProperty.getUsername(), dataSourceProperty.getPassword()); this.dataSourceProperty = dataSourceProperty; }
@Override protected Map<String, DataSourceProperty> executeStmt(Statement statement) throws SQLException { Map<String, DataSourceProperty> dataSourcePropertiesMap = new HashMap<>(); ResultSet resultSet = statement.executeQuery("select * from library_sync_config where is_used = true"); if (Objects.nonNull(resultSet)) { while (resultSet.next()) { DataSourceProperty newProperty = BeanUtil.copyProperties(dataSourceProperty, DataSourceProperty.class); String version = resultSet.getString("version"); String configName = resultSet.getString("config_name"); String url = replaceDatabaseName(dataSourceProperty.getUrl(), configName); newProperty.setPoolName(version); dataSourcePropertiesMap.put(version, newProperty); } } return dataSourcePropertiesMap; }
private String replaceDatabaseName(String url, String newDatabaseName) { String oldDatabaseName = parseDatabaseName(url); return url.replaceFirst("(?i)" + oldDatabaseName, newDatabaseName); }
private static String parseDatabaseName(String url) { Matcher matcher = PATTERN.matcher(url);
if (matcher.find()) { return matcher.group(1); } else { return null; } } }
|
这里因为我是通过更新知识库操作再创建一个同级别的数据库,所以除了库名其他信息都是一致的,新连接只需要替换库名即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Configuration @EnableConfigurationProperties({DynamicDataSourceProperties.class}) public class DynamicDataSourceConfig { private final DynamicDataSourceProperties properties;
public DynamicDataSourceConfig(DynamicDataSourceProperties properties) { this.properties = properties; }
@Bean @Primary public DynamicDataSourceProvider dynamicDataSourceProvider(DefaultDataSourceCreator defaultDataSourceCreator) { Map<String, DataSourceProperty> datasource = this.properties.getDatasource(); DataSourceProperty master = datasource.get("master"); return new CustomDynamicDataSourceProvider(defaultDataSourceCreator, master); } }
|
通过控制台打印的日志我们也可以看到,虽然配置文件只配置了一个数据连接,但系统启动时通过其它地方新增了一个配置连接。
自定义处理逻辑
动态数据源通过拦截添加了 @DS(xxx)
注解的类或方法,根据注解设置的值选取对应数据源连接。
通过配置类 DynamicDataSourceAopConfiguration
发现有两个主要的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Bean @ConditionalOnMissingBean public DsProcessor dsProcessor(BeanFactory beanFactory) { DsProcessor headerProcessor = new DsHeaderProcessor(); DsProcessor sessionProcessor = new DsSessionProcessor(); DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor(); spelExpressionProcessor.setBeanResolver(new BeanFactoryResolver(beanFactory)); headerProcessor.setNextProcessor(sessionProcessor); sessionProcessor.setNextProcessor(spelExpressionProcessor); return headerProcessor; }
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) @Bean @ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX + ".aop", name = "enabled", havingValue = "true", matchIfMissing = true) public Advisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) { DynamicDatasourceAopProperties aopProperties = properties.getAop(); DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(aopProperties.getAllowedPublicOnly(), dsProcessor); DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor, DS.class); advisor.setOrder(aopProperties.getOrder()); return advisor; }
|
DsProcessor
是一个处理器的抽象类,我们只需要实现何时匹配这个处理器,以及根据传入的key决定要匹配哪个数据源连接。
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
| public abstract class DsProcessor {
private DsProcessor nextProcessor;
public void setNextProcessor(DsProcessor dsProcessor) { this.nextProcessor = dsProcessor; }
public abstract boolean matches(String key);
public String determineDatasource(MethodInvocation invocation, String key) { if (matches(key)) { String datasource = doDetermineDatasource(invocation, key); if (datasource == null && nextProcessor != null) { return nextProcessor.determineDatasource(invocation, key); } return datasource; } if (nextProcessor != null) { return nextProcessor.determineDatasource(invocation, key); } return null; }
public abstract String doDetermineDatasource(MethodInvocation invocation, String key); }
|
所以我们只需要在配置文件中创建自己的 Processor
即可自定义数据源的处理逻辑,当然,我们也可以定义多个 Processor
形成一条执行链。
我们定义好自己的 Processor
之后因为 @ConditionalOnMissingBean
注解默认的处理器就不会生效了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Bean public DsProcessor dsProcessor() { return new MyProcessor(); }
public class MyProcessor extends DsProcessor { @Override public boolean matches(String key) { return true; }
@Override public String doDetermineDatasource(MethodInvocation invocation, String key) { return null; } }
|
动态增减数据源
DynamicRoutingDataSource
组件中提供了 addDataSource()
和 removeDataSource()
两个方法,使用时只需要注入该组件调用指定方法即可,十分方便。