使用场景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