基于springboot的多数据源管理
背景
开发过程中,一个应用中长面林多个数据源场景。springboot提供的默认数据源装配只针对单数据源场景。本文基于springboot+mysql+mybatis+aop实现多数据源切换
实现重点
AbstractRoutingDataSource
spring本省提供的多数据源管理类。
主要关注
setTargetDataSources,设置多数据源,将数据源统一管理,底层是一个Map结构,key数据源key,value为数据源配置dataSource
determineCurrentLookupKey,从多数据源挑选需要切换的数据源
DataSourceRoutingDataSource
继承AbstractRoutingDataSource,实现determineCurrentLookupKey方法,根据业务挑选对应数据源
public class DataSourceRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
final DataSourceKey dataSourceKey = DynamicDataSourceContextHolder.getDataSourceKey();
log.info("Current DataSource is [{}]", dataSourceKey);
return dataSourceKey;
}
}
DynamicDataSourceContextHolder
数据源key上下文持有者,使用ThreadLocal存储
public class DynamicDataSourceContextHolder {
/**
* 用于在切换数据源时保证不会被其他线程修改
*/
private static Lock lock = new ReentrantLock();
/**
* 用于轮循的计数器
*/
private static int counter = 0;
private static final ThreadLocal<Object> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* All DataSource List
*/
public static List<Object> dataSourceKeys = new ArrayList<>();
/**
* The constant slaveDataSourceKeys.
*/
public static List<Object> slaveDataSourceKeys = new ArrayList<>();
public static void setDataSourceKey(DataSourceKey key) {
CONTEXT_HOLDER.set(key);
}
public static void useMobileDataSource() {
CONTEXT_HOLDER.set(DataSourceKey.DS0);
}
public static DataSourceKey getDataSourceKey() {
DataSourceKey key = (DataSourceKey) CONTEXT_HOLDER.get();
return key==null?DataSourceKey.DS0:key;
}
public static void clearDataSourceKey() {
CONTEXT_HOLDER.remove();
}
}
DataSourceConfigurer
多数据源配置类,用于装载配置文件中配置的多数据源,比如放置在application.yml
spring:
datasource:
druid:
ds0:
name: ds0
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:13306/ds0?serverTimezone=UTC&characterEncoding=utf-8&useSSL=false
type: com.alibaba.druid.pool.DruidDataSource
ds1:
name: ds1
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:13306/ds1?serverTimezone=UTC&characterEncoding=utf-8&useSSL=false
type: com.alibaba.druid.pool.DruidDataSource
ds2:
name: ds2
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:13306/ds2?serverTimezone=UTC&characterEncoding=utf-8&useSSL=false
type: com.alibaba.druid.pool.DruidDataSource
# mybatis 配置
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations:
- classpath:mybatis/transaction.xml
具体实现如下
@Configuration
@EnableTransactionManagement(order = Ordered.LOWEST_PRECEDENCE) // 确保事务切面最后执行
public class DataSourceConfigurer {
/**
* @Primary 注解用于标识默认使用的 DataSource Bean,因为有多个 DataSource Bean,该注解可用于 master
* 或 slave DataSource Bean, 但不能用于 dynamicDataSource Bean, 否则会产生循环调用
* @ConfigurationProperties 注解用于从 application.properties 文件中读取配置,为 Bean 设置属性
* @return data source
*/
@Primary
@Bean("ds0")
@ConfigurationProperties("spring.datasource.druid.ds0")
public DataSource ds0(){
return DruidDataSourceBuilder.create().build();
}
@Bean("ds1")
@ConfigurationProperties("spring.datasource.druid.ds1")
public DataSource ds1(){
return DruidDataSourceBuilder.create().build();
}
@Bean("ds2")
@ConfigurationProperties("spring.datasource.druid.ds2")
public DataSource ds2(){
return DruidDataSourceBuilder.create().build();
}
@Bean("dynamicDataSource")
public DataSource dynamicDataSource(@Qualifier(value = "ds0") DataSource ds0,
@Qualifier(value = "ds1") DataSource ds1,
@Qualifier(value = "ds2") DataSource ds2){
DataSourceRoutingDataSource dataSourceRoutingDataSource = new DataSourceRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>(10);
dataSourceMap.put(DataSourceKey.DS0,ds0);
dataSourceMap.put(DataSourceKey.DS1,ds1);
dataSourceMap.put(DataSourceKey.DS2,ds2);
// 默认指定的数据源
dataSourceRoutingDataSource.setDefaultTargetDataSource(ds0);
// 将 order 和 其它 数据源作为指定的数据源
dataSourceRoutingDataSource.setTargetDataSources(dataSourceMap);
return dataSourceRoutingDataSource;
}
/**
* 配置 SqlSessionFactoryBean
* @ConfigurationProperties 在这里是为了将 MyBatis 的 mapper 位置和持久层接口的别名设置到
* Bean 的属性中,如果没有使用 *.xml 则可以不用该配置,否则将会产生 invalid bond statement 异常
*
* @return the sql session factory bean
*/
@Bean
@ConfigurationProperties(prefix = "mybatis")
@ConditionalOnBean(name = "dynamicDataSource")
public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 配置数据源,此处配置为关键配置,如果没有将 dynamicDataSource 作为数据源则不能实现切换
sqlSessionFactoryBean.setDataSource(dynamicDataSource);
return sqlSessionFactoryBean;
}
/**
* 注入 DataSourceTransactionManager 用于事务管理
*/
@Bean
@ConditionalOnBean(name = "dynamicDataSource")
public PlatformTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dynamicDataSource);
transactionManager.setTransactionSynchronization(DataSourceTransactionManager.SYNCHRONIZATION_ALWAYS);
return transactionManager;
}
}
RoutingWith
多数据源切换注解,可放置在方法或者类上
@Target({ElementType.TYPE,ElementType.METHOD,ElementType.ANNOTATION_TYPE,ElementType.FIELD,ElementType.PACKAGE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RoutingWith {
DataSourceKey routerKey() default DataSourceKey.DS0;
}
DataSourceKey 为多数据源key,后续使用切面拦截,获取当前注解中的DataSourceKey,设置到holder上下文,在DataSourceRoutingDataSource中进行数据源选择时从上下文holder中获取,完成数据源切换
DynamicDataSourceAspect
数据源注解切面,完成注解key获取并设置,为后续数据源切换提供key支持
需要注意点为:
多数据源在面对事物时不做特殊处理会导致切换失败,其原如下
1、spring事务管理在方法开始时确定数据源,后续不在切换
2、事务开启后持有当前数据源连接,导致无法切换
3、事务AOP和数据源切换AOP的执行顺序可能不正确,需要保证数据源切换AOP在事务之前执行
@Slf4j
@Aspect
@Component
@Order(Ordered.LOWEST_PRECEDENCE - 1) // 确保在事务切面前执行
public class DynamicDataSourceAspect {
/**
* 针对类级别拦截
* @param joinPoint
* @param routingWith
* @return
* @throws Throwable
*/
@Before("@within(routingWith)")
public void routingWithDataSourceBefore(JoinPoint joinPoint, RoutingWith routingWith) throws Throwable {
doBefore(routingWith);
}
@Before("@within(routingWith)")
public void routingWithDataSourceAfter(JoinPoint joinPoint, RoutingWith routingWith) throws Throwable {
doAfter();
}
/**
* 针对方法级别拦截
* @param joinPoint
* @param routingWith
* @return
* @throws Throwable
*/
@Before("@annotation(routingWith)")
public void routingWithDataSourceMBefore(JoinPoint joinPoint, RoutingWith routingWith) throws Throwable {
doBefore(routingWith);
}
@After("@annotation(routingWith)")
public void routingWithDataSourceMAfter(JoinPoint joinPoint, RoutingWith routingWith) throws Throwable {
doBefore(routingWith);
}
private void doBefore(RoutingWith routingWith){
DataSourceKey key = routingWith.routerKey();
if (DynamicDataSourceContextHolder.getDataSourceKey()!=key){
// 设置当前数据源
DynamicDataSourceContextHolder.setDataSourceKey(key);
// 注册事务同步回调
if (TransactionSynchronizationManager.isActualTransactionActive()){
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCompletion(int status) {
DynamicDataSourceContextHolder.clearDataSourceKey();
}
});
}
}
}
private void doAfter() {
if (!TransactionSynchronizationManager.isActualTransactionActive()) {
DynamicDataSourceContextHolder.clearDataSourceKey();
}
}

浙公网安备 33010602011771号