在最近的开发中需要在业务数据库之外访问大数据提供的数据,所以使用到了多数据源。下面就讲一下在SpringBoot中如何配置多数据源。
一、方法介绍
我们使用org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
来完成数据源的切换。在AbstractRoutingDataSource
中Spring使用Map来管理数据源,在对象初始化完成后会将配置的对象放入Map<Object, DataSource> resolvedDataSources
。
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<Object, DataSource>(this.targetDataSources.size());
for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) {
Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
this.resolvedDataSources.put(lookupKey, dataSource);
}
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
既然在存储时使用到了map,那么获取的时候获取的时候我们也得提供这样一个key。
/**
* Retrieve the current target DataSource. Determines the
* {@link #determineCurrentLookupKey() current lookup key}, performs
* a lookup in the {@link #setTargetDataSources targetDataSources} map,
* falls back to the specified
* {@link #setDefaultTargetDataSource default target DataSource} if necessary.
* @see #determineCurrentLookupKey()
*/
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;
}
/**
* Determine the current lookup key. This will typically be
* implemented to check a thread-bound transaction context.
* <p>Allows for arbitrary keys. The returned key needs
* to match the stored lookup key type, as resolved by the
* {@link #resolveSpecifiedLookupKey} method.
*/
protected abstract Object determineCurrentLookupKey();
从代码中我们可以看到,这个key是由抽象方法determineCurrentLookupKey提供,所以我们需要重写这个方法,提供我们自己生成key的方法。
二、实现多数据源选择器
首先,我们定以一个枚举用来存放数据源的key。
public enum DataSourceType {
// 大数据源
BigData,
// 主业务源
Business;
}
这里我设置了BigData
和Business
两个key,分别用来标识主业务数据和大数据的数据源。现在我们继承AbstractRoutingDataSource
类并重写determineCurrentLookupKey
方法。
public class DynamicDataSource extends AbstractRoutingDataSource {
// 数据源类型集合
private static final List<DataSourceType> dataSourceTypes = Lists.newArrayList();
//线程本地环境
private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>();
//设置数据源
public static void setDataSourceType(DataSourceType routingType) {
contextHolder.set(routingType);
}
public static void addDataSourceType(DataSourceType dataSourceType) {
if (dataSourceType != null) {
dataSourceTypes.add(dataSourceType);
}
}
public static void reset() {
contextHolder.remove();
}
public static boolean containsDataSource(DataSourceType routingType) {
return dataSourceTypes.contains(routingType);
}
@Override
protected Object determineCurrentLookupKey() {
return contextHolder.get();
}
}
在上面的动态数据源中使用ThreadLocal来存放当前的数据源类型,保证每一个线程都获取到自己想要的数据源类型。下面我们通过切面来指定数据源。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {
DataSourceType type();
}
@Aspect
@Order(-1)
@Component
public class DynamicDataSourceAspect {
@Around("@annotation(targetDataSource)")
public Object targetDataSource(ProceedingJoinPoint pjp, TargetDataSource targetDataSource) throws Throwable {
DataSourceType dataSourceType = targetDataSource.type();
if (DynamicDataSource.containsDataSource(dataSourceType)) {
DynamicDataSource.setDataSourceType(targetDataSource.type());
}
try {
return pjp.proceed();
} finally {
DynamicDataSource.reset();
}
}
}
对于使用了TargetDataSource
注解的方法,我们通过环绕通知(Around)在方法调用前设置ThreadLocal中的数据源类型为注解中指定的类型,因为处在统一个线程中,之后determineCurrentLookupKey
方法就能够获取到指定的key,进而得到我们需要的数据源。
三、配置数据源
前面我们已经设置好了多数据元切换的选择器了,现在我们要提供出多个数据源,并将它们放入到选择器当中。具体方法如下:
@Configuration
@MapperScan(basePackages = "com.song.study.multidatasource.db.mapper")
@PropertySource(value = "classpath:dataSource.properties")
public class DynamicDataSourceConfig implements ImportBeanDefinitionRegistrar, EnvironmentAware {
private DataSource businessDS;
private DataSource bigDateDS;
@Override
public void setEnvironment(Environment environment) {
businessDS = initDataSource(environment, DataSourceType.BigData.name());
bigDateDS = initDataSource(environment, DataSourceType.Business.name());
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
Map<Object, Object> targetDataSources = new HashMap();
targetDataSources.put(DataSourceType.Business, businessDS);
targetDataSources.put(DataSourceType.BigData, bigDateDS);
DynamicDataSource.addDataSourceType(DataSourceType.BigData);
DynamicDataSource.addDataSourceType(DataSourceType.Business);
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(DynamicDataSource.class);
beanDefinition.setSynthetic(true);
MutablePropertyValues mpv = beanDefinition.getPropertyValues();
mpv.addPropertyValue("targetDataSources", targetDataSources);
registry.registerBeanDefinition("dataSource", beanDefinition);
}
public DataSource initDataSource(Environment environment, String prefix) {
RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(environment, prefix);
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setPoolName(prefix + "HikariDataSourcePool");
hikariConfig.setDriverClassName(propertyResolver.getProperty(".database.driverClassName"));
hikariConfig.setJdbcUrl(propertyResolver.getProperty(".database.url"));
hikariConfig.setUsername(propertyResolver.getProperty(".database.username"));
hikariConfig.setPassword(propertyResolver.getProperty(".database.password"));
hikariConfig.setMaxLifetime(propertyResolver.getProperty(".connection.maxLifeTime", Integer.class, 120000));
hikariConfig.setConnectionTimeout(propertyResolver.getProperty(".connection.timeout", Integer.class, 2000));
hikariConfig.setMinimumIdle(propertyResolver.getProperty(".pool.minPoolSize", Integer.class, 20));
hikariConfig.setMaximumPoolSize(propertyResolver.getProperty(".pool.maxPoolSize", Integer.class, 300));
hikariConfig.setConnectionInitSql("SELECT 1");
HikariDataSource dataSource = new HikariDataSource(hikariConfig);
return dataSource;
}
}
四、使用数据源
接下来只要在调用数据库操作的方法上添加TargetDataSource
注解就好了。
@Service
public class SampleServiceImpl implements SampleService {
@Autowired
private BusinessRepository businessRepository;
@Autowired
private BigDataRepository bigDataRepository;
@Override
@TargetDataSource(type = DataSourceType.Business)
public BusinessPO getBusiness(Long id) {
return businessRepository.getById(id);
}
@Override
@TargetDataSource(type = DataSourceType.BigData)
public BigDataPO getBigData(Long id) {
return bigDataRepository.getById(id);
}
}
完整example见Github(iceSong/multi-data-source)。