Spring Boot项目多数据源配置

使用场景java


多数据源的使用在分库的状况下并不稀奇,而平时的项目需求就不多见了。之前我也没去琢磨过,只是前阵子项目新的需求恰好须要,并且还不是同一种数据库。mysql


个人奇葩新需求须要实现三个数据库动态切换,一个是你们都知道的mysql,一个是亚马逊的Redshift,还有一个也是亚马逊的服务Athena。为了都能使用mybatis,须要实现自动识别数据库从而切换数据源。有一个特殊,因为Athena的jdbc奇葩,mybatis并不兼容,只能使用古版的jdbc直操方式,但也不影响使用动态数据源。算法


Spring Boot项目中多数据源的配置spring


为了简单,我会缩减一些你们不用看也明白的代码。我将以实现三个不一样数据库的数据源动态切换为例子,更贴近真实需求,讲述如何在spring boot项目中配置多数据源。但愿各位看官可以耐心看完。sql


我选择抛弃spring boot提供的jdbc配置,本身定义多数据源的jdbc链接参数配置。而且我也加入了链接池的支持,多个数据源可使用同一份链接池配置,前提是多个数据源都使用同一种链接池,如阿里云的druid链接池。网上不少都没有介绍到多数据源链接池的配置,不要紧,看我这篇就够了。数据库


一、配置jdbc链接信息编程


Mysql数据源的配置
设计模式

mysql:
    driverClassName: com.mysql.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:3306/xxxx?characterEncoding=utf8&useSSL=false
    username: root
    password:

Redshift数据源配置mybatis

redshift:
   driverClassName: com.amazon.redshift.jdbc42.Driver
   jdbcUrl: jdbc:redshift://xxx.xxxx.com:5439/xxxx
   username: xxxxx
   password: xxxxx

AWS Athena数据源配置多线程

athena:
  username: xxxx
  password: xxxx
  aws-region: xxxx
  s3-outputlocation: s3://xxxxx/
  url: jdbc:awsathena://AwsRegion=%s;UID=%s;PWD=%s;S3OutputLocation=%s;
  driver: com.simba.athena.jdbc.Driver


二、配置链接池信息


在resources下建立一个链接池配置文件,我选择使用properties文件,由于我要实现本身读取配置信息。

druid.properties 数据库链接池配置信息。由于mysql和redshift均可以使用druid链接池,而Athena是个奇葩,就无论了,由于也不多用到,因此不给Athena配置链接池。

dataSource.initialSize=20
dataSource.minIdle=1
dataSource.maxIdle=20
dataSource.maxActive=100
dataSource.maxWait=60000


我还为此写了一个properties配置读取工具,实现自动读取指定配置信息,并映射为java对象返回。这里涉及到两个类,一个是PropertiesAnnotation,另外一个是PropertiesUtils。前者是一个注解,后者实现读取配置信息并使用反射建立对象为对象的字段赋值,详细介绍请往下看。

先来建立一个用于接收数据库链接池配置信息的java类。字段名须要与配置文件中的字段名相同。并使用@PropertiesAnnotation注解声明须要从哪一个文件读取,配置的前缀是什么。你可能不理解前缀是什么,好比前面的数据库链接池配置:dataSource.initialSize,那么前缀就是dataSource,若是没有前缀则写""。

    @PropertiesAnnotation(filePath = "database/druid.properties", prefix = "dataSource")
    @NoArgsConstructor
    @Data
    public class DruidConfig {
        private Integer initialSize;
        private Integer minIdle;
        private Integer maxIdle;
        private Integer maxActive;
        private Integer maxWait;
    }

建立PropertiesAnnotation注解,本应该是先建立该注解的,但我为了习惯你们的理解顺序(按浏览顺序理解)。PropertiesAnnotation声明为运行时使用在类上的注解,只有两个属性,一个是文件基于classpath的路径,另外一个就是配置信息的前缀,已经在前面说过了。

/**
 * @author wujiuye
 * @version 1.0 on 2019/4/24 {描述:}
 */

@Target({ElementType.TYPE})
@Retention(RUNTIME)
@Documented
public @interface PropertiesAnnotation {
    /**
     * 文件路径,基于classpath
     */

    String filePath();

    /**
     * 属性名前缀
     */

    String prefix();
}

PropertiesUtils实现的功能就是自动读取解析properties配置文件,只须要传递你用来接收配置信息的java类便可。getPropertiesConfig方法会读取java类(Class)上的@PropertiesAnnotation注解,并从注解中获取到文件的路径,以及配置的前缀信息,如database,而后读取配置文件,从配置文件中找到前缀为database的配置。根据反射生成一个java对象,并将配置信息赋值给java对象中对应的字段。一时看不明白不要紧,能够跳过日后看。

/**
 * @author wujiuye
 * @version 1.0 on 2019/4/24 {描述:}
 */

public class PropertiesUtils {
    /**
     * 获取配置文件内容
     *
     * @return
     */

    public static <T> T getPropertiesConfig(Class<T> configClass) throws Exception {
        ClassLoader loader = configClass.getClassLoader();
        T obj = configClass.newInstance();
        Properties properties = new Properties();
        //获取注解信息
        PropertiesAnnotation propertiesAnnotation = configClass.getAnnotation(PropertiesAnnotation.class);
        if (propertiesAnnotation == null) {
            throw new Exception("not found @PropertiesAnnotation annotation!!!");
        }
        if (StringUtil.isEmpty(propertiesAnnotation.filePath())) {
            throw new Exception("file path is null!!!");
        }
        String prefix = propertiesAnnotation.prefix();
        if (StringUtil.isEmpty(prefix)) {
            prefix = "";
        } else {
            prefix += '.';
        }
        try (InputStream in = loader.getResourceAsStream(propertiesAnnotation.filePath())) {
            properties.load(new InputStreamReader(in"utf-8"));
            Field[] fields = configClass.getDeclaredFields();
            if (fields == null || fields.length == 0) {
                return obj;
            }
            for (Field field : fields) {
                field.setAccessible(true);
                String value = properties.getProperty(prefix + field.getName());
                if (field.getType() == Integer.class || field.getType() == int.class) {
                    field.set(obj, Integer.valueOf(value));
                } else if (field.getType() == Long.class || field.getType() == long.class) {
                    field.set(obj, Long.valueOf(value));
                } else if (field.getType() == Boolean.class || field.getType() == boolean.class) {
                    field.set(obj, Boolean.valueOf(value));
                } else {
                    field.set(obj, value);
                }
            }
        } catch (Exception e) {
            throw e;
        }
        return obj;
    }

}



三、先配置这三个数据源


如何获取数据源的配置信息不用说了吧,最简单的可使用@Value。如mysql数据源配置信息,其它的本身花几秒钟脑补。

    @Value("${mysql.driverClassName}")
    private String driverClassName;
    @Value("${mysql.jdbcUrl}")
    private String jdbcUrl;
    @Value("${mysql.username}")
    private String username;
    @Value("${mysql.password}")
    private String password;

建立一个数据源的配置类DataSourceConfiguration。
a、配置mysql数据源

    //mysql数据源
    @Bean(name = "mysql-database")
    public DataSource mysqlDatabase() {
        DruidDataSource druidDataSource = new DruidDataSource();
        //配置jdbc
        druidDataSource.setDriverClassName(driverClassName);
        druidDataSource.setUrl(jdbcUrl);
        druidDataSource.setUsername(username);
        druidDataSource.setPassword(password);
        //配置链接池信息
        PropertiesUtils.DruidConfig druidConfig = PropertiesUtils.getPropertiesConfig(PropertiesUtils.DruidConfig.class);
       druidDataSource.setMinIdle(druidConfig.getMinIdle());
       druidDataSource.setMaxWait(druidConfig.getMaxWait());
       druidDataSource.setMaxActive(druidConfig.getMaxActive());
       druidDataSource.setInitialSize(druidConfig.getInitialSize());
       .....
        return druidDataSource;
    }

Redshift数据源的配置同mysql数据源的配置,这里要说的是另外一个奇葩数据源Athena的配置。

    //athena数据源,比较特殊
    @Bean(name = "athena-database")
    public DataSource athenaDatabase() {
        //加载athena的驱动类
        try {
            Class.forName(ATHENA_DRIVER);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        com.simba.athena.jdbc.DataSource dataSource = new com.simba.athena.jdbc.DataSource();
        String url = String.format(ATHENA_URL, ATHENA_REGION, ATHENA_USERNAME, ATHENA_PASSWORD, ATHENA_S3_OUTPUTLOCATION);
        dataSource.setURL(url);
        return dataSource;
    }



四、配置动态数据源


这里须要使用spring jdbc框架提供的一个类AbstractRoutingDataSource,这是一个让咱们轻松实现多数据切换使用的抽象类,须要咱们继承该类,实现具体的切换逻辑。AbstractRoutingDataSource不须要引入多余的依赖,这是spring框架自己提供的,若是你的项目使用不了这个类,那就应该是spring boot版本的问题了。


/**
 * @author wujiuye
 * @version 1.0 on 2019/4/17 {描述:
 * 使用Spring的AbstractRoutingDataSource实现多数据源切换
 * }
 */

public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * 动态设置数据源
     * 配置多数据源时map的key
     * @return
     */

    @Override
    protected Object determineCurrentLookupKey() {
       System.out.println("数据源为" + DataSourceContextHolder.getDataSource());
        return DataSourceContextHolder.getDataSource();
    }

}

determineCurrentLookupKey方法就是用来实现具体的切换逻辑的,你会看到,这里只是用了DataSourceContextHolder.getDataSource()方法返回当前须要使用的数据源。由于一次数据库访问操做是在一个线程内完成的,因此我使用ThreadLocal来存储当前jdbc线程应该使用哪一个数据源,而具体使用哪一个数据须要在mapper方法执行以前使用AOP设置,后面具体讲。

determineCurrentLookupKey方法返回的是数据源的key,由于多个数据源是使用一个map存储的,你也能够理解为spring管理bean的name,但最好不要这么理解,稍后看动态数据源dynamicDataSource配置的时候就能明白了。

/**
     * 动态数据源: 经过AOP在不一样数据源之间动态切换
     *
     * @return
     */

    @Primary
    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 默认数据源,当没有使用@DataSource注解时使用,
        // 而使用了@DataSource注解若是没有设置beanName也要Aop本身配置使用默认的bean
        dynamicDataSource.setDefaultTargetDataSource(mysqlDatabase());
        // 配置多数据源
        // key -> bean
        Map<Object, Object> dsMap = new HashMap();
        dsMap.put("mysqlDatabase", mysqlDatabase());
        dsMap.put("athenaDatabase", athenaDatabase());
        dynamicDataSource.setTargetDataSources(dsMap);
        return dynamicDataSource;
    }

    /**
     * 配置@Transactional事物注解
     * 使用动态数据源
     *
     * @return
     */

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource());
    }

注意,事务的管理也须要配置为使用动态数据源,不然@Transactional的使用会有意想不到的意外。但其实你配置了也有意外,就是使用事务以后动态数据源就不生效了。

/**
 * @author wujiuye
 * @version 1.0 on 2019/4/17 {描述:}
 */

public class DataSourceContextHolder {

    private final static String DEFAULT_DATASOURCE = "mysqlDatabase";

    public static String getDefaultDataSourceBeanName() {
        return DataSourceContextHolder.DEFAULT_DATASOURCE;
    }

    /**
     * 保存的是多数据源Map的key
     */

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    // 设置数据源Key
    public static void setDataSource(String dataSourceKey{
        //System.out.println("切换到{" + dataSourceKey + "}数据源");
        contextHolder.set(dataSourceKey);
    }

    // 获取当前线程应该使用的数据源的key
    public static String getDataSource() {
        return (contextHolder.get());
    }

    // 清除当前线程使用的数据源的key
    public static void clearDataSource() {
        contextHolder.remove();
    }

}

五、实现数据源的动态切换

其实,到第四步多数据源就算配置完成了,可是最关键的动态根据业务切换数据源尚未实现。若是你认为这样就完了,那么就只会永远使用默认的数据源。


如何使用AOP实现动态数据源的切换,固然是实现一个切面,定义一个切点,而切点就是全部mapper(mybatis)接口中的方法。使用基于注解的切点会很容易实现这一需求。先建立一个注解@DataSource,只须要一个属性便可,用来标明该方法使用哪一个数据源。


/**
 * @author wujiuye
 * @version 1.0 on 2019/4/17 {描述:
 * 用于切换数据源
 * }
 */

@Target({ElementType.METHOD})
@Retention(RUNTIME)
@Documented
public @interface DataSource {
    //数据源的Key
    String value();
}

接着就是实现AOP类,咱们并不须要干涉具体的数据库执行增删改查的操做,因此不要使用环绕加强,使用前置加强便可。同时由于使用ThreadLocal存储切换的数据源,应该在方法执行完成以后清除设置,避免污染其它未使用@database注解的mapper方法,应为线程重用问题。AOP具体的实现代码以下。

@Aspect
@Component
public class DynamicDataSourceAop {

    /**
     * 定义数据源切点
     */

    @Pointcut("@annotation(com.hippo.cayman.annotation.DataSource)")
    public void dataSourcePointcut() {
    }

    /**
     * 根据注解动态设置数据源,没有注解的使用默认数据源
     *
     * @param point
     */

    @Before("dataSourcePointcut()&&@annotation(dataSource)")
    public void beforeSettingDataSource(JoinPoint point, DataSource dataSource) {
        String dataSourceKey = DataSourceContextHolder.getDefaultDataSourceKey();
        try {
            if (dataSource.value() != null) {
                dataSourceKey = dataSource.value();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 切换数据源
        DataSourceContextHolder.setDataSource(dataSourceKey);
    }

    /**
     * 方法执行完成以后须要移除设置的数据源(ThreadLocal保存的要清除)
     */

    @After("dataSourcePointcut()")
    public void afterRemoveSetting() {
        DataSourceContextHolder.clearDataSource();
    }
}


注意一点,方法执行异常也须要清除数据源的设置。


/**
     * 执行异常也须要清除
     */

    @AfterThrowing("dataSourcePointcut()")
    public void afterExceptionSetting() {
        DataSourceContextHolder.clearDataSource();
    }


六、使用


使用很简单,在mapper类的方法上使用注解声明须要使用的数据源便可。


public interface UserMapper {

    @DataSource("athenaDatabase")
    @Select("select * from sys_account where username=#{username} limit 1")
    User selectByUsername(String username);

}


当前存在的缺点


不支持使用@Transactional事务,由于@Transactional是加在Service层的。


一、使用事务时,数据源切换失败。

使用了@Transactional,则每次都会从TransactionUtils的ThreadLocal中去拿数据源,若是为空,就建立新的链接,若是不为空的话直接使用,而ThreadLocal是线程的变量,由于ThreadLocal不为空,因此这就会致使数据源切换失败。


二、使用事务,同时也使用线程池时须要注意

若是你使用了数据库链接线程池,那么前面第一点的问题你就很难解决了。关于第一点网页有不少解决方法,可是我目前还不须要用到,若是我用到我会去研究源码再给你们分享解决方案,在此先埋下一个坑,由于确实没有时间折腾这东西。


总结


多数据源的配置就介绍到这里,若是想玩出花样,还须要本身去琢磨,不本身思考是感觉不到那种成功的喜悦的。有时候也须要有种动力逼迫本身去学习,好比换工做带来的竞争压力,再好比主动挑战复杂的业务需求。也能够是主动去优化项目代码。个人代码优势是能合理的应用设计模式,将对外提供的功能尽量封装,让使用更简单,让代码阅读更清晰,同时,也会考虑到扩展性、兼容性,能使用算法的地方尽量的使用,多线程编程须要考虑性能,jdk新特性要用好,其实这些我以前也发过一篇文章专门聊代码优化的。




https://mp.weixin.qq.com/s/_OK3AgYoyoqu7QHY8QWLjw

相关文章
相关标签/搜索