在开发过程中我们经常会遇到一个项目有需要使用多个数据源,下面就来搭建一个多数据源的项目。
项目结构
创建spring boot聚合工程可以参考:
https://blog.csdn.net/tianshuhao521/article/details/83353392
该项目是一个spring boot的聚合工程
bushro-web——>bushro-service——>bushro-mapper——>bushro-pojo
在数据库中分别创建两个数据库,master,slave,用来测试,分别创建user表,写入测试数据。
项目配置yml
bushro-web中application.yml配置
server: port: 8080 spring: datasource: hikari: master: driver-class-name: com.mysql.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource jdbc-url: jdbc:mysql://localhost/master?useSSL=false&serverTimezone=UTC username: root password: 671354 filters: stat,wall,log4j2 maxActive: 50 initialSize: 5 maxWait: 60000 minIdle: 1 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true maxOpenPreparedStatements: 20 slave: driver-class-name: com.mysql.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource jdbc-url: jdbc:mysql://localhost/slave?useSSL=false&serverTimezone=UTC username: root password: 671354 filters: stat,wall,log4j2 maxActive: 10 initialSize: 5 maxWait: 60000 minIdle: 1 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true maxOpenPreparedStatements: 20 mybatis: mapper-locations: classpath:/mapper/*.xml type-aliases-package: com.bushro.pojo #pagehelper分页插件 pagehelper: helperDialect: oracle reasonable: true supportMethodsArguments: true params: count=countSql
配置数据源
- DataSourceKey.java,数据库标识类,与bushro-web中的application.yml中的hikari下的名字一致
动态切换数据源的主要配置在bushro-mapper上,有6个类
DataSourceKey:
/** * 数据库标识 */ public enum DataSourceKey { /** * Master data source key. */ master, /** * Slave alpha data source key. */ slave, }
- DataSourceRoutingDataSource.java
该类继承自 AbstractRoutingDataSource 类,在访问数据库时会调用该类的 determineCurrentLookupKey() 方法获取数据库实例的 key
package com.bushro.config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * Multiple DataSource Configurer * */ public class DynamicRoutingDataSource extends AbstractRoutingDataSource { private final Logger logger = LoggerFactory.getLogger(getClass()); /** * Set dynamic DataSource to Application Context * * @return */ @Override protected Object determineCurrentLookupKey() { logger.debug("Current DataSource is [{}]", DynamicDataSourceContextHolder.getDataSourceKey()); return DynamicDataSourceContextHolder.getDataSourceKey(); } }
- DataSourceConfigurer.java
数据源配置类,在该类中生成多个数据源实例并将其注入到 ApplicationContext 中
package com.bushro.config; import org.mybatis.spring.SqlSessionFactoryBean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * Multiple DataSource Configurer */ @Configuration public class DataSourceConfigurer { /** * master DataSource * @Primary 注解用于标识默认使用的 DataSource Bean,因为有多个个 DataSource Bean, * 该注解可用于 master或 slave DataSource Bean, 但不能用于 dynamicDataSource Bean, 否则会产生循环调用 * @ConfigurationProperties 注解用于从 application.properties 文件中读取配置,为 Bean 设置属性 * @return data source */ @Bean("master") @Primary @ConfigurationProperties(prefix = "spring.datasource.hikari.master") public DataSource master() { return DataSourceBuilder.create().build(); } /** * Slave DataSource * * @return the data source */ @Bean("slave") @ConfigurationProperties(prefix = "spring.datasource.hikari.slave") public DataSource slave() { return DataSourceBuilder.create().build(); } /** * 动态数据源 * * @return the data source */ @Bean("dynamicDataSource") public DataSource dynamicDataSource() { DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource(); Map<Object, Object> dataSourceMap = new HashMap<>(2); dataSourceMap.put(DataSourceKey.master.name(), master()); dataSourceMap.put(DataSourceKey.slave.name(), slave()); // 将 master 数据源作为默认指定的数据源 dynamicRoutingDataSource.setDefaultTargetDataSource(master()); // 将 master 和 slave 数据源作为指定的数据源 dynamicRoutingDataSource.setTargetDataSources(dataSourceMap); // 将数据源的 key 放到数据源上下文的 key 集合中,用于切换时判断数据源是否有效 DynamicDataSourceContextHolder.dataSourceKeys.addAll(dataSourceMap.keySet()); // 将 Slave 数据源的 key 放在集合中,用于轮循 DynamicDataSourceContextHolder.slaveDataSourceKeys.addAll(dataSourceMap.keySet()); DynamicDataSourceContextHolder.slaveDataSourceKeys.remove(DataSourceKey.master.name()); return dynamicRoutingDataSource; } /** * 配置 SqlSessionFactoryBean * @ConfigurationProperties 在这里是为了将 MyBatis 的 mapper 位置和持久层接口的别名设置到 * Bean 的属性中,如果没有使用 *.xml 则可以不用该配置,否则将会产生 invalid bond statement 异常 * @return the sql session factory bean */ @Bean public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception{ SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); // 配置 MyBatis sqlSessionFactoryBean.setTypeAliasesPackage("com.bushro.mapper"); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*Mapper.xml")); // 配置数据源,此处配置为关键配置,如果没有将 dynamicDataSource 作为数据源则不能实现切换 sqlSessionFactoryBean.setDataSource(dynamicDataSource()); return sqlSessionFactoryBean; } /** * 注入 DataSourceTransactionManager 用于事务管理 */ @Bean public PlatformTransactionManager transactionManager() { return new DataSourceTransactionManager(dynamicDataSource()); } }
- DynamicDataSourceContextHolder.java
该类为数据源上下文配置,用于切换数据源
package com.bushro.config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; /** * 多个数据源上下文容器 * */ public class DynamicDataSourceContextHolder { private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class); private static int counter = 0; /** * Maintain variable for every thread, to avoid effect other thread */ private static final ThreadLocal<String> CONTEXT_HOLDER = ThreadLocal.withInitial(DataSourceKey.master::name); /** * All DataSource List */ public static List<Object> dataSourceKeys = new ArrayList<>(); /** * The constant slaveDataSourceKeys. */ public static List<Object> slaveDataSourceKeys = new ArrayList<>(); /** * To switch DataSource * * @param key the key */ public static void setDataSourceKey(String key) { CONTEXT_HOLDER.set(key); } /** * Use master data source. */ public static void useMasterDataSource() { CONTEXT_HOLDER.set(DataSourceKey.master.name()); } /** * Use slave data source. */ public static void useSlaveDataSource() { try { int datasourceKeyIndex = counter % slaveDataSourceKeys.size(); CONTEXT_HOLDER.set(String.valueOf(slaveDataSourceKeys.get(datasourceKeyIndex))); counter++; } catch (Exception e) { logger.error("Switch slave datasource failed, error message is {}", e.getMessage()); useMasterDataSource(); e.printStackTrace(); } } /** * Get current DataSource * * @return data source key */ public static String getDataSourceKey() { return CONTEXT_HOLDER.get(); } /** * To set DataSource as default */ public static void clearDataSourceKey() { CONTEXT_HOLDER.remove(); } /** * Check if give DataSource is in current DataSource list * * @param key the key * @return boolean boolean */ public static boolean containDataSourceKey(String key) { return dataSourceKeys.contains(key); } }
- DynamicDataSourceAspect.java
动态数据源切换的切面,切 DAO 层,通过 DAO 层方法名判断使用哪个数据源,实现数据源切换 关于切面的 Order 可以可以不设,因为 @Transactional 是最低的,取决于其他切面的设置,并且在 org.springframework.core.annotation.AnnotationAwareOrderComparator 会重新排序
package com.bushro.config; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; @Aspect // 该切面应当先于 @Transactional 执行 @Order(-1) @Component public class DynamicDataSourceAspect { /** * Switch DataSource * * @param point * @param targetDataSource */ @Before("@annotation(targetDataSource))") public void switchDataSource(JoinPoint point, TargetDataSource targetDataSource) { if (!DynamicDataSourceContextHolder.containDataSourceKey(targetDataSource.value())) { System.out.println("DataSource [{}] doesn't exist, use default DataSource [{}] " + targetDataSource.value()); } else { // 切换数据源 DynamicDataSourceContextHolder.setDataSourceKey(targetDataSource.value()); System.out.println("Switch DataSource to [{}] in Method [{}] " + DynamicDataSourceContextHolder.getDataSourceKey() + point.getSignature()); } } /** * Restore DataSource * * @param point * @param targetDataSource */ @After("@annotation(targetDataSource))") public void restoreDataSource(JoinPoint point, TargetDataSource targetDataSource) { // 将数据源置为默认数据源 DynamicDataSourceContextHolder.clearDataSourceKey(); System.out.println("Restore DataSource to [{}] in Method [{}] " + DynamicDataSourceContextHolder.getDataSourceKey() + point.getSignature()); } }
- TargetDataSource
注解
package com.bushro.config; import java.lang.annotation.*; /** *注解来标识使用哪个数据库 */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface TargetDataSource { String value(); }
bushro-pojo创建user类,bushro-mapper创建相应的xml,以及dao层接口
服务层
在相应的方法上使用@TargetDataSource
注解来表明使用哪个数据库
@Service public class UserServiceImpl implements UserService { @Resource UserMapper userMapper; @TargetDataSource("master") @Override public List getUserMaster() { return userMapper.getUserList(); } @TargetDataSource("slave") @Override public List getUserSlave() { return userMapper.getUserList(); } }
controller
@RestController public class UserController { @Resource UserService userService; @RequestMapping("/getUserMaster") public List getUserMaster(){ return userService.getUserMaster(); } @RequestMapping("/getUserSlave") public List getUserSlave(){ return userService.getUserSlave(); } }
结果
两个方法调用的都是同一个方法,但是从不同数据库取出来的结果就不同了