Poison

Multiple Datasource

最近项目中需要接入多个数据源,起初准备让 Mybatis 来进行数据源的动态选择,但查询 Mybatis 的相关文档后,未发现官方对多数据源的支持,并且笔者的项目中 Mybatis 的使用采用全注解的方式,如果实例化两个 SqlSessionFactory ,在全注解使用 Mybatis 的情况下,无法显式指定某个 Mapper 使用哪一个 sqlSessionFactory 。

继续查询相关资料,发现 Spring 自 2.0.1 开始就提供了对动态数据源的支持,参见 Dynamic DataSource Routing ,咳咳,由于文章历史悠久,追溯至2007年,所以在笔者的项目中笔者对其进行了改进,思路依然与原文基本一致,只不过把文中基于XML的相关配置改为了我们项目中的基于 Java Code 的配置。

为了保护隐私,以下代码部分命名被修改

  1. 定义多个数据源的枚举,本例中仅包含两个数据源

    1
    2
    3
    4
    5
    6
    7
    /**
    * Created by Poison on 8/15/2016.
    */
    enum DataSource {
    FIRST,
    SECOND
    }
  2. 定义 DataSourceContextHolder

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    /**
    * Created by Poison on 8/15/2016.
    */
    class DataSourceContextHolder {
    private static ThreadLocal<DataSource> contextHolder = new ThreadLocal<>();
    static void setDataSource(DataSource dataSource) {
    contextHolder.set(dataSource);
    }
    static DataSource getDataSource() {
    return contextHolder.get();
    }
    static void restoreToFirstDataSource() {
    contextHolder.set(DataSource.FIRST);
    }
    }
  3. 定义 RoutingDataSource

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    * Created by Poison on 8/15/2016.
    */
    class RoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
    return DataSourceContextHolder.getDataSource();
    }
    }
  4. Datasource 相关配置

    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
    64
    65
    66
    67
    68
    69
    70
    71
    72
    @Bean
    public javax.sql.DataSource dataSourceForFirst() {
    HikariConfig hikariConfig = new HikariConfig();
    hikariConfig.setDataSourceClassName(getProperty("jdbc.dataSourceClassName"));
    hikariConfig.setUsername(getProperty("jdbc.username"));
    hikariConfig.setPassword(getProperty("jdbc.password"));
    hikariConfig.setMinimumIdle(Integer.parseInt(getProperty("jdbc.minimumIdle")));
    hikariConfig.setMaximumPoolSize(Integer.parseInt(getProperty("jdbc.maximumPoolSize")));
    hikariConfig.addDataSourceProperty("serverName", getProperty("jdbc.serverName"));
    hikariConfig.addDataSourceProperty("port", getProperty("jdbc.port"));
    hikariConfig.addDataSourceProperty("databaseName", getProperty("jdbc.databaseName"));
    optimizeHikariConfigForMysql(hikariConfig);
    return new HikariDataSource(hikariConfig);
    }
    private void optimizeHikariConfigForMysql(HikariConfig hikariConfig) {
    hikariConfig.addDataSourceProperty("cachePrepStmts", true);
    hikariConfig.addDataSourceProperty("prepStmtCacheSize", 250);
    hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", 2048);
    }
    @Bean
    public javax.sql.DataSource dataSourceForSecond() {
    HikariConfig hikariConfig = new HikariConfig();
    hikariConfig.setDataSourceClassName(env.getProperty("jdbc.second.dataSourceClassName"));
    hikariConfig.setUsername(getProperty("jdbc.second.username"));
    hikariConfig.setPassword(getProperty("jdbc.second.password"));
    hikariConfig.setMinimumIdle(Integer.parseInt(env.getProperty("jdbc.second.minimumIdle")));
    hikariConfig.setMaximumPoolSize(Integer.parseInt(env.getProperty("jdbc.second.maximumPoolSize")));
    hikariConfig.addDataSourceProperty("serverName", getProperty("jdbc.second.serverName"));
    hikariConfig.addDataSourceProperty("port", getProperty("jdbc.second.port"));
    hikariConfig.addDataSourceProperty("databaseName", getProperty("jdbc.second.databaseName"));
    optimizeHikariConfigForMysql(hikariConfig);
    return new HikariDataSource(hikariConfig);
    }
    @Bean
    public javax.sql.DataSource dataSource() {
    RoutingDataSource routingDataSource = new RoutingDataSource();
    Map<Object, Object> targetDataSources = new HashMap<>();
    targetDataSources.put(DataSource.FIRST, dataSourceForFirst());
    targetDataSources.put(DataSource.SECOND, dataSourceForSecond());
    routingDataSource.setTargetDataSources(targetDataSources);
    routingDataSource.setDefaultTargetDataSource(dataSourceForFirst());
    return routingDataSource;
    }
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
    SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
    sessionFactory.setDataSource(dataSource());
    sessionFactory.setTypeAliasesPackage("me.tianshuang.domain");
    sessionFactory.setTypeHandlers(new TypeHandler[]{new LocalDateTimeTypeHandler()});
    org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
    configuration.setMapUnderscoreToCamelCase(true);
    sessionFactory.setConfiguration(configuration);
    return sessionFactory.getObject();
    }
  5. AOP 相关配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * Created by Poison on 8/15/2016.
    */
    @Aspect
    @Component
    public class AroundDataSource {
    @Around("execution(* me.tianshuang.dao.second..*.*(..))")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
    DataSourceContextHolder.setDataSource(DataSource.SECOND);
    Object result = pjp.proceed();
    DataSourceContextHolder.restoreToFirstDataSource();
    return result;
    }
    }

在对数据源进行路由的控制这方面,采用了 AOP 的思想,在我们的项目中,因为第二个数据源用的频率很低,所以为需要操作第二个数据源的 Mapper 单独建立了一个包 (me.tianshuang.dao.second) ,在此包下的所有 Mapper 的方法在执行前将切换数据源为 SECOND ,执行完方法后又切回数据源 First 。

以上只是切换数据源的一种方案,本文的关键就在于 RoutingDataSource ,而在哪里切换数据源,读者完全可根据自身项目需要选择最适合的方案,该例只是适合笔者所在项目的一个例子。