Spring中的多数据源
在实际开发中,有很多场景可能会用到多个数据库,当然这个多个数据库存在两种情况:
1. 同一类型数据库管理系统中的不同数据库:MySQL DB1 和 MySQL DB2
2. 不同类型数据库管理系统中的不同数据库:MySQL DB1 和 MsSQL DB2
LZ的开发场景就是上述情况2,用户基本信息存储在MySQL数据库中,业务原始数据(数据量庞大)存储在MsSQL数据库中。于是在执行业务逻辑的时候必然会设计多数据源情况,在这里浅谈一下Spring中多数据源的理解。
查阅资料结合当前Guns集合框架:
DatabaseContextHolder是一个线程安全的DatabaseType容器,并提供了向其中设置和获取DatabaseType的方法。
public class DataSourceContextHolder {
//为每一个线程创建一个副本
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
/**
* 设置数据源类型
*
* @param dataSourceType 数据库类型
*/
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
/**
* 获取数据源类型
*/
public static String getDataSourceType() {
return contextHolder.get();
}
/**
* 清除数据源类型
*/
public static void clearDataSourceType() {
contextHolder.remove();
}
}
我们进入线程对象contentHolder中的get()方法发现:数据源信息的存储是通过map进行存储的。
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
DynamicDataSource继承AbstractRoutingDataSource并重写其中的方法determineCurrentLookupKey(),在该方法中使用DatabaseContextHolder获取当前线程的DatabaseType。
//动态数据源
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
*
* 先调用determineCurrentLookupKey()方法获取一个数据源
* 获取数据源:先根据设置去targetDataSources中去找,若没有,则选择defaultTargetDataSource
* /
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
MyBatisPlusConfig中生成多个数据源DataSource的bean#####
该配置类中注入的对象分别为默认数据源配置和另一个数据源的配置,项目采用的druid连接池技术,数据源初始化工作完成后都要加入连接池。
多数据源的开关设置在application.yml配置文件中,采用@ConditionalOnProperty注解方式进行控制配置是否生效。
public class MybatisPlusConfig {
@Autowired
DruidProperties druidProperties;
@Autowired
MutiDataSourceProperties mutiDataSourceProperties;
//数据源初始化参数构造配置...
private DruidDataSource bizDataSource() {
DruidDataSource dataSource = new DruidDataSource();
druidProperties.config(dataSource);
mutiDataSourceProperties.config(dataSource);
return dataSource;
}
/**
* 多数据源连接池配置
*/
@Bean
@ConditionalOnProperty(prefix = "guns", name = "muti-datasource-open", havingValue = "true")
public DynamicDataSource mutiDataSource() {
//创建druid连接池数据源对象
DruidDataSource dataSourceGuns = dataSourceGuns();
DruidDataSource bizDataSource = bizDataSource();
//数据源初始化
try {
dataSourceGuns.init();
bizDataSource.init();
}catch (SQLException sql){
sql.printStackTrace();
}
//多数据源配置 -- 详情见下步
}
}
MyBatisPlusConfig中将key-value对写入到DynamicDataSource动态数据源的targetDataSources属性。
接上文中的代码:
//创建动态数据源对象
DynamicDataSource dynamicDataSource = new DynamicDataSource();
//采用map进行DataSourceType存储,key-value 这里的key是通过枚举类定义的常量标识符
HashMap<Object, Object> hashMap = new HashMap();
hashMap.put(DSEnum.DATA_SOURCE_GUNS, dataSourceGuns);
hashMap.put(DSEnum.DATA_SOURCE_BIZ, bizDataSource);
//设置动态数据源 -- 多数据源模式下需要设置默认数据源
dynamicDataSource.setTargetDataSources(hashMap);
dynamicDataSource.setDefaultTargetDataSource(dataSourceGuns);
return dynamicDataSource;
将DynamicDataSource作为primary数据源注入到SqlSessionFactory的dataSource属性中去,并将该dataSource作为transactionManager的入参来构造DataSourceTransactionManager#####
public class DataSourceTransactionManager extends AbstractPlatformTransactionManager
implements ResourceTransactionManager, InitializingBean {
private DataSource dataSource;
private boolean enforceReadOnly = false;
/**
* Create a new DataSourceTransactionManager instance.
* A DataSource has to be set to be able to use it.
* @see #setDataSource
*/
public DataSourceTransactionManager() {
setNestedTransactionAllowed(true);
}
/**
* Create a new DataSourceTransactionManager instance.
* @param dataSource JDBC DataSource to manage transactions for
*/
//dataSource作为transactionManager的入参构造DataSourceTransactionManager
public DataSourceTransactionManager(DataSource dataSource) {
this();
setDataSource(dataSource);
afterPropertiesSet();
}
}
关于使用:自定义注解 + AOP进行实现
Guns自定义的注解@DataSource(name = DSEnum.XXX) + MultiSourceExAop切面进行多数据源的切换。
这里将使用时候遇到的问题和该MultiSourceExAop切面一并进行解释说明。
关于多数据源切换问题,在MultiSourceExAop中进行了声明。
1. @DataSource注解只能用于方法,在业务方法名上添加注解,即表示为切换数据源。
2. 多数据源切换逻辑:
//根据注解中name属性的值,从注解中获得数据源类型。
DataSource datasource = currentMethod.getAnnotation(DataSource.class);
if (datasource != null) {
//数据源切换
DataSourceContextHolder.setDataSourceType(datasource.name());
log.debug("设置数据源为:" + datasource.name());
} else {
DataSourceContextHolder.setDataSourceType(mutiDataSourceProperties.getDefaultDataSourceName());
log.debug("设置数据源为:dataSourceCurrent");
}
try {
//正常业务逻辑
return point.proceed();
} finally {
log.debug("清空数据源信息!");
DataSourceContextHolder.clearDataSourceType();
}
可以看出,在切面方法中,每次都会根据注解获取数据源类型,并进行数据源切换。而且,无论业务逻辑是否执行,最终都会清空数据源信息。这里指的清空,是变更为默认数据源类型:MySQL;
3. 方法中嵌套使用多数据源:这里的内外调用方法均添加了@DataSource注解
经过debug发现,DataSourceContextHolder中的线程为单线程,即一次存储单个数据源信息。
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
在嵌套使用多数据源的场景中:存在两种情况。
1. 外调用的数据源切换已完成后内数据源进行数据源切换。
/**
* 获取证券交易列表
*/
@RequestMapping(value = "/list")
@ResponseBody
@DataSource(name = DSEnum.DATA_SOURCE_BIZ) //外调用
public Object list(@RequestParam(value = "condition", defaultValue = "") String condition,
@RequestParam(value = "tradingDay", defaultValue = "") String tradingDay) {
if (condition.equals("") && tradingDay.equals("")) {
return tradingStockMapper.selectPage(new Page<TradingStock>(1, 10), null);
}
//内调用
return iSecurityDealService.getTradingStockList(condition, tradingDay.replaceAll("-+",""));
}
2. 外调用的数据源切换未完成,内数据源的数据源进行切换,此时新建了一个数据库链接。外调用不可正常使用。
/**
* 新增证券交易
*/
@RequestMapping(value = "/add")
@ResponseBody
@DataSource(name = DSEnum.DATA_SOURCE_BIZ) //外调用
public Object add(TradingStock tradingStock) {
//内调用
if (iProductBasicInfoService.getProductInfoByProductId(tradingStock.getProductID()) != null){
tradingStock.setOpdate(new Date());
tradingStock.setAmount((double) (tradingStock.getVolume() * tradingStock.getPrice()));
//外调用未完成业务逻辑 -- 出错
tradingStockMapper.insert(tradingStock);
}
return super.SUCCESS_TIP;
}
4.解决方案:
涉及数据源切换的通用逻辑:ProductInfoCheck,将该方法重写后不添加@DataSource注解。
其他方法同理,当外调用已经开启注解进行了数据源切换,内调用的方法就不应该再添加@DataSource注解。
但由于该种嵌套切换数据源局限于部分操作,因此可以采用该方法进行解决。
AOP作用域为当前切点(方法),之前提到的注释数据源关闭时不恰当的,数据源的连接应根据所需进行开关,避免造成资源浪费。