spring boot 动态数据源配置 & 运行时新增数据源 场景:
同一系统支持不同业务场景,需要用到不同的数据库。
各个用户/应用之间数据完全隔离(不同的用户/应用,不同的数据库),而一个程序需要支持不同的用户/应用。例如 一些paas服务需要支持不同的业务场景,但是不同项目之间数据、账号、权限、token等业务数据都是完全隔离的,仅共享机器资源。
一、spring boot 动态数据源配置 方案:在一个确切的地方存储 数据源的配置信息(我是将这些信息存储在一个 配置数据库表中,而这个配置数据库是确切的,作为主数据源配置)。启动spring时,会初始化这个配置数据源,然后将其他动态数据源信息取出来初始化好datasource 注册到spring 容器。
原理:主要是实现AbstractRoutingDataSource的抽象类,然后将该类注册到spring容器,其中关键点是:
配置AbstractRoutingDataSource类的默认数据源Object defaultTargetDataSource
和其他数据源Map<Object, Object> targetDataSources
。targetDataSources就是我们动态配置的数据源,key-value 接口,后面根据key 查找 datasource
实现determineCurrentLookupKey()方法,该方法决定了当前操作选择哪个数据源
注册到spring 容器
如何确定数据源的选择:业务层面通过API request中附带的参数(header、session、cookie、url_param等)来判断此次请求对应的数据源是哪个?例如 url?appid=1 ,那么就判断此次请求是appid=1 的应用库。第二步,确定后,将appid=1 对应的数据源key 存入本地线程ThreadLocal中。后面在determineCurrentLookupKey方法中 从本地线程ThreadLocal中取出对应的key。spring 会根据该key 选择对应的datasource 作为接下来操作的数据源。
原理大致这样,可以根据业务场景调整数据源选择的设计逻辑
数据源的初始化 配置AbstractRoutingDataSource类的默认数据源Object defaultTargetDataSource
和其他数据源Map<Object, Object> targetDataSources
。targetDataSources就是我们动态配置的数据源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private void initDataSource (Environment env) throws Exception { propertyResolver = new RelaxedPropertyResolver(env, "spring.datasource." ); dataSource = DataSourceBuilder.create() .driverClassName(propertyResolver.getProperty("driver-class-name" )) .url(propertyResolver.getProperty("url" )) .username(propertyResolver.getProperty("username" )) .password(propertyResolver.getProperty("password" )) .type((Class<? extends DataSource>) Class.forName(propertyResolver.getProperty("type" ))) .build(); dataBinder(dataSource, env); logger.info("init primaryDataSource" );
其中 数据源初始化时,需要配置相关信息比如说连接池、超时时间等,需要调用 dataBinder
方法对数据源参数进行bind操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 spring.datasource.name=datasource spring.datasource.url=jdbc:mysql: spring.datasource.username=${db_user} spring.datasource.password=${db_pwd} spring.datasource.type=com.zaxxer.hikari.HikariDataSource spring.datasource.driver-class -name =com.mysql.jdbc.Driver spring.datasource.maximum-pool-size=20 spring.datasource.filters=stat spring.datasource.maxActive=20 spring.datasource.initialSize=1 spring.datasource.maxWait=10000 spring.datasource.minIdle=1 spring.datasource.timeBetweenEvictionRunsMillis=60000 spring.datasource.minEvictableIdleTimeMillis= 300000 spring.datasource.validationQuery=select 1 spring.datasource.testWhileIdle=true spring.datasource.testOnBorrow=true spring.datasource.testOnReturn=true spring.datasource.poolPreparedStatements=true spring.datasource.maxOpenPreparedStatements=20
自定义的databind方法,传入 需绑定的datasource 和 获取参数用的Environment
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private void dataBinder (DataSource dataSource, Environment env) { RelaxedDataBinder dataBinder = new RelaxedDataBinder(dataSource); dataBinder.setConversionService(conversionService); dataBinder.setIgnoreNestedProperties(false ); dataBinder.setIgnoreInvalidFields(false ); dataBinder.setIgnoreUnknownFields(true ); if (dataSourcePropertyValues == null ) { Map<String, Object> rpr = new RelaxedPropertyResolver(env, "spring.datasource" ).getSubProperties("." ); Map<String, Object> values = new HashMap<>(rpr); values.remove("type" ); values.remove("driver-class-name" ); values.remove("url" ); values.remove("username" ); values.remove("password" ); values.remove("name" ); dataSourcePropertyValues = new MutablePropertyValues(values); } dataBinder.bind(dataSourcePropertyValues); }
动态数据源处理 上面提到AbstractRoutingDataSource
,以下是其部分源码。 关注determineTargetDataSource()
方法、determineCurrentLookupKey()
(需重写)、afterPropertiesSet
方法。其中代码中的 resolvedDataSources
是通过targetDataSource
(key-datasource的Map)赋值的,通过setTargetDataSources
设置进去后会调用afterPropertiesSet()
方法设置为resolvedDataSources
,按照上述逻辑实现determineCurrentLookupKey
方法,找到数据源即可
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 public void setTargetDataSources (Map<Object, Object> targetDataSources) { this .targetDataSources = targetDataSources; } public void setDefaultTargetDataSource (Object defaultTargetDataSource) { this .defaultTargetDataSource = defaultTargetDataSource; } protected DataSource determineTargetDataSource () { Assert.notNull(this .resolvedDataSources, "DataSource router not initialized" ); Object lookupKey = this .determineCurrentLookupKey(); DataSource 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 + "]" ); } else { return dataSource; } } protected abstract Object determineCurrentLookupKey () ;
重写的determineCurrentLookupKey
方法:(其中Constant.{XXXX}
只是定义好的的字符串常量)。自己实现DynamicDataSource类
1 2 3 4 5 6 7 8 9 public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey () { String appointDataSource = ThreadLocalUtil.get(Constant.APPOINT_DATA_SOURCE); return (StringUtil.isBlank(appointDataSource) ? "dataSource" : appointDataSource); } }
Request拦截器拦截处理,确定选择哪个数据源,将数据源key 存储到本地线程中的Map中,determineCurrentLookupKey
则是从线程的Map中取出key。(本例中:数据库名称 = db_{数据源key} = db_{appkey}
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 String appKey = request.getParameter(Constant.WEB_DEFAULT_APPKEY); ThreadLocalUtil.set(Constant.APPOINT_DATA_SOURCE, appKey); private static final ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal() { protected Map<String, Object> initialValue () { return new HashMap(4 ); } }; public static void set (String key, Object value) { Map map = (Map)threadLocal.get(); map.put(key, value); }
注册到Spring 容器 核心:自定义类 DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar
。 实现registerBeanDefinitions
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Override public void registerBeanDefinitions (AnnotationMetadata annotaion,BeanDefinitionRegistry registry) { logger.info("registerBeanDefinitions" ); GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); beanDefinition.setBeanClass(DynamicDataSource.class ) ; beanDefinition.setSynthetic(true ); MutablePropertyValues mpv = beanDefinition.getPropertyValues(); mpv.addPropertyValue("defaultTargetDataSource" , dataSource); mpv.addPropertyValue("targetDataSources" ,targetDataSources); registry.registerBeanDefinition("dataSource" , beanDefinition); }
在spring boot 应用启动时
1 @Import (DynamicDataSourceRegister.class )
关于事务 和 子线程 注意: AbstractRoutingDataSource 只支持单库事务,也就是说切换数据源要在开启事务之前执行。 spring DataSourceTransactionManager进行事务管理,开启事务,会将数据源缓存到DataSourceTransactionObject对象中进行后续的commit rollback等事务操作。
二、运行时动态添加数据源 运行时,动态添加数据源,按照上述逻辑,只需要将新的datasource添加到targetDataSource的Map中,然后通知更新
举例方案:开放一个接口,通过接口形式调用;主要做两件事1.往数据库里面插入新数据源配置信息;2.将该数据源初始化好放到targetDataSource的Map中,然后通知已经更新数据源
我在配置信息表中只存储了appkey 也就是dbname。动态数据源我都用统一的用户、密码和连接池,这些信息都存在配置文件中。
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 @GetMapping ("/api/project/register" ) @ResponseBody public String register (@RequestParam String appKey) { ApplicationContext ctx = SpringUtil.getApplicationContext(); DynamicDataSource dynamicDataSource = ctx.getBean(DynamicDataSource.class ) ; String url = sysProperties.getDbAppUrl().replace("{dbName}" , "db_" + appKey); DataSource ds = DataSourceBuilder.create() .driverClassName(sysProperties.getDbAppDriverClass()) .url(url) .username(sysProperties.getDbAppUserName()) .password(sysProperties.getDbAppPassword()) .type(com.zaxxer.hikari.HikariDataSource.class ) .build () ; dynamicDataSource.addDataSourceToTargetDataSource(appKey, ds); return "success" ; }
class DynamicDataSource
新增以下内容。
DynamicDataSource
中多加了一个ConcurrentHashMap<String, DataSource> backupTargetDataSources
,用于后续的动态添加数据源时做targetDataSources的备份,因为无法获取targetDataSources,只能设置targetDataSources。
private ConcurrentHashMap<String, DataSource> backupTargetDataSources = new ConcurrentHashMap<>();
public void addDataSourceToTargetDataSource (String key ,DataSource ds) {
this .backupTargetDataSources.put(key, ds);
this .setTargetDataSource(this .backupTargetDataSources);
}
public void setTargetDataSource (Map targetDataSource) {
super .setTargetDataSources(targetDataSource);
this .afterPropertiesSet();
}
这样当程序需要支持一个新应用/新场景时,只需要通过脚本完成下面两步:1.建数据库,初始化好数据。2. 调用接口:curl ${baseurl}/api/project/register?appKey=xxxx
以上,只按照思路记录关键code,并不是完整的代码