Spring-jdbc 提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。
AbstractRoutingDataSource的getConnection() 方法根据查找 lookup key 键对不同目标数据源的调用,通常是通过(但不一定)某些线程绑定的事物上下文来实现。
AbstractRoutingDataSource的多数据源动态切换的核心逻辑是:在程序运行时,把数据源数据源通过 AbstractRoutingDataSource 动态织入到程序中,灵活的进行数据源切换。
基于AbstractRoutingDataSource的多数据源动态切换,可以实现读写分离,这么做缺点也很明显,无法动态的增加数据源。
实现逻辑:
定义DynamicDataSource类继承抽象类AbstractRoutingDataSource,并实现了determineCurrentLookupKey()方法。
把配置的多个数据源会放在AbstractRoutingDataSource的 targetDataSources和defaultTargetDataSource中,然后通过afterPropertiesSet()方法将数据源分别进行复制到resolvedDataSources和resolvedDefaultDataSource中。
调用AbstractRoutingDataSource的getConnection()的方法的时候,先调用determineTargetDataSource()方法返回DataSource在进行getConnection()。
spring.datasource.master.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.master.hikari.minimum-idle=5
spring.datasource.master.hikari.maximum-pool-size=30
spring.datasource.master.hikari.auto-commit=true
spring.datasource.master.hikari.idle-timeout=30000
spring.datasource.master.hikari.pool-name=SCMMasterHikariCP
spring.datasource.master.hikari.max-lifetime=1800000
spring.datasource.master.hikari.connection-timeout=30000
spring.datasource.master.hikari.connection-test-query=SELECT 1
spring.datasource.slave.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.slave.hikari.minimum-idle=2
spring.datasource.slave.hikari.maximum-pool-size=10
spring.datasource.slave.hikari.auto-commit=true
spring.datasource.slave.hikari.idle-timeout=30000
spring.datasource.slave.hikari.pool-name=SCMSlaveHikariCP
spring.datasource.slave.hikari.max-lifetime=1800000
spring.datasource.slave.hikari.connection-timeout=30000
spring.datasource.slave.hikari.connection-test-query=SELECT 1
spring.datasource.master.jdbc-url=jdbc:mysql://localhost:3306/supply_center?useSSL=false&characterEncoding=UTF-8&autoReconnect=true
spring.datasource.master.username=root
spring.datasource.master.password=1234
spring.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.slave.jdbc-url=jdbc:mysql://localhost:3306/supply_center?useSSL=false&characterEncoding=UTF-8&autoReconnect=true
spring.datasource.slave.username=root
spring.datasource.slave.password=1234
spring.datasource.slave.driver-class-name=com.mysql.cj.jdbc.Driver
@Configuration
public class DataSourceConfig {
@Bean(name = "masterDataSource")
@ConfigurationProperties("spring.datasource.master")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "slaveDataSource")
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DBTypeEnum.MASTER, masterDataSource);
targetDataSources.put(DBTypeEnum.SLAVE, slaveDataSource);
MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();
myRoutingDataSource.setDefaultTargetDataSource(masterDataSource);//设置默认数据源
myRoutingDataSource.setTargetDataSources(targetDataSources);//设置数据源
return myRoutingDataSource;
}
}
@EnableTransactionManagement
@Configuration
public class MyBatisConfig {
@Resource(name = "myRoutingDataSource")
private DataSource myRoutingDataSource;
@Autowired
private MybatisProperties mybatisProperties;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
sqlSessionFactoryBean.setMapperLocations(mybatisProperties.resolveMapperLocations());
sqlSessionFactoryBean.setConfiguration(mybatisProperties.getConfiguration());
return sqlSessionFactoryBean.getObject();
}
@Bean
public PlatformTransactionManager platformTransactionManager() {
return new DataSourceTransactionManager(myRoutingDataSource);
}
}
public enum DBTypeEnum {
MASTER, SLAVE;
}
@Slf4j
public class DBContextHolder {
private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();
public static void set(DBTypeEnum dbType) {
contextHolder.set(dbType);
}
public static DBTypeEnum get() {
return contextHolder.get();
}
public static void clear() {
contextHolder.remove();
}
public static void master() {
set(DBTypeEnum.MASTER);
log.info("change Data Source to:{}", get());
}
public static void slave() {
set(DBTypeEnum.SLAVE);
log.info("change Data Source to:{}", get());
}
}
public class MyRoutingDataSource extends AbstractRoutingDataSource {
/**
* Determine the current lookup key. This will typically be
* implemented to check a thread-bound transaction context.
* Allows for arbitrary keys. The returned key needs
* to match the stored lookup key type, as resolved by the
* {@link #resolveSpecifiedLookupKey} method.
*/
@Override
protected Object determineCurrentLookupKey() {
return DBContextHolder.get()==null?DBTypeEnum.MASTER:DBContextHolder.get();
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
DBTypeEnum type() default DBTypeEnum.SLAVE;
}
@Aspect
@Component
@Order(0)
public class DataSourceAspect {
@Pointcut("(@annotation(com.thermofisher.dsc.scm.datasource.DataSource))")
public void pointcut() {
}
@Before("pointcut()")
public void read(JoinPoint joinPoint) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
DataSource source = method.getAnnotation(DataSource.class);//获取注解内容
switch (source.type()) {
case MASTER:
DBContextHolder.master();
break;
case SLAVE:
DBContextHolder.slave();
break;
default:
DBContextHolder.master();
}
log.info("当前数据源:{}", DBContextHolder.get());
}
@After("pointcut()")
public void after(JoinPoint point) {
//清理掉当前设置的数据源,让默认的数据源不受影响
log.info("清理数据源 当前源:{}", DBContextHolder.get());
DBContextHolder.clear();
}
}
对应方法添加数据源注解,便可实现数据源灵活切换
@Override
@DataSource(type = DBTypeEnum.SLAVE)
public List<CityModel> selectAllCityInfo() {
return cityMapper.selectCityInfo(null);
}