SpringBoot配置多数据源

在最近的开发中需要在业务数据库之外访问大数据提供的数据,所以使用到了多数据源。下面就讲一下在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;
}

这里我设置了BigDataBusiness两个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)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容