Spring boot Mybatis-Plus Dynamic Connection (自動切換資料源)


Spring boot Mybatis Plus Dynamic change Connection


說明:

    Spring boot 自動切換連線Databases 資料源,可以透過 Spring jdbc AbstractRoutingDataSource 切換資料源,如果想要深入了解怎麼執行運做可以看一下Spring jdbc source code ,接下來我們來說說如何運用 AbstractRoutingDataSource 做到自動切換資料源。


首先Databases是Master與Slave,自動切換讀寫分離

定義兩個 annotation @EnableMybatisCluster @MybatisClusterMaster  

@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.TYPE)
@Import({ MultiDataSourceConfig.class, MybatisPlusConfig.class})
public @interface EnableMybatisCluster {
}

當Query 需要指定為Master執行時使用,直接加在ServiceImpl mehtod 
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MybatisClusterMaster {
}

創建 Constant , 取讀Databases yaml  名稱
public interface DataSourceConstant {
interface Master {
String JDBC_VALUE = "spring.dynamic.datasource.master";

String DATA_SOURCE = "dataSourceMaster";
}

interface Slave {
String JDBC_VALUE = "spring.dynamic.datasource.slave";

String DATA_SOURCE = "dataSourceSlave";
}

interface ResourceMapper {
String CLASSPATH_PATH = "classpath:mapper/*.xml";
}

}

創建 Enums , 如果本身有多台Slave,可自行加入多台Slave名稱。
public enum DBTypeEnum {
/**
* MASTER, SLAVE
*/
MASTER, SLAVE;
}


創建DataSourceConfig ,建立Bean Master , Slave DataSource , DataSource 預設值為Master,未指定時都以Master為主。

@Configuration
@Slf4j
public class MultiDataSourceConfig {

/**
* DataSource
*
* @return Master DataSource
*/
@Bean(name = DataSourceConstant.Master.DATA_SOURCE)
@ConfigurationProperties(prefix = DataSourceConstant.Master.JDBC_VALUE)
@Primary
public DataSource dataSourceMaster() {
return DataSourceBuilder.create().build();
}

/**
* DataSource
*
* @return Slave1 DataSource
*/
@Bean(name = DataSourceConstant.Slave.DATA_SOURCE)
@ConfigurationProperties(prefix = DataSourceConstant.Slave.JDBC_VALUE)
public DataSource dataSourceSlave() {
return DataSourceBuilder.create().build();
}


/**
* default master
* @param masterDataSource 主要資料庫
* @param slaveDataSource 備用資料庫
* @return DataSource
*/
@Bean
public DataSource myRoutingDataSource(@Qualifier(DataSourceConstant.Master.DATA_SOURCE) DataSource masterDataSource,
@Qualifier(DataSourceConstant.Slave.DATA_SOURCE) DataSource slaveDataSource) {
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
targetDataSources.put(DBTypeEnum.MASTER, masterDataSource);
targetDataSources.put(DBTypeEnum.SLAVE, slaveDataSource);
MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();
myRoutingDataSource.setDefaultTargetDataSource(masterDataSource);//設置預設為master
myRoutingDataSource.setTargetDataSources(targetDataSources);
return myRoutingDataSource;
}

}

創建 Mybatis-Plus SqlSessionFactory, 其中有使用到Mybatis Plugin Interceptor , 後續會說明為什麼會需要。

@EnableTransactionManagement
@Configuration
public class MybatisPlusConfig {

@Resource(name = "myRoutingDataSource")
private DataSource myRoutingDataSource;

@Resource
private MybatisPluginInterceptor mybatisPluginInterceptor;

/**
* @return sqlSessionFactory
* @throws Exception
*/
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(DataSourceConstant.ResourceMapper.CLASSPATH_PATH));

sqlSessionFactoryBean.setPlugins(mybatisPluginInterceptor );
// MybatisConfiguration mybatisConfiguration = new MybatisConfiguration();
// sqlSessionFactoryBean.setConfiguration(mybatisConfiguration);
return sqlSessionFactoryBean.getObject();
}


/**
* 事務配置
* @return 事務管理器
*/
@Bean
public DataSourceTransactionManager transactionManager() {
DataSourceTransactionManager tx = new DataSourceTransactionManager();
tx.setDataSource(myRoutingDataSource);
return tx;
}
}


創建 MyRoutingDataSource ,繼承AbstractRoutingDataSource 
@Slf4j
public class MyRoutingDataSource extends AbstractRoutingDataSource {
@Nullable
@Override
protected Object determineCurrentLookupKey() {
log.debug("線程[{}],切换到的資料庫為:{}", Thread.currentThread().getId(), DBContextHolder.get());
return DBContextHolder.get();
}
}

創建 DBContextHolder , 自動切換目前資料源,假設有多台Slave可自行定義調整連線數超過一定數量後更換其他台Slave。
@Slf4j
public class DBContextHolder {

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

private static final AtomicInteger counter = new AtomicInteger(-1);

public static void set(DBTypeEnum dbType) {
contextHolder.set(dbType);
}

public static DBTypeEnum get() {
return contextHolder.get();
}

public static void master() {
remove();
log.info("使用的資料庫: MASTER ...");
set(DBTypeEnum.MASTER);
}

/**
* 如果有多台slave可切換,default 1台slave
*/
public static void slave() {
remove();
log.info("使用的資料庫: SLAVE ....");
int index = counter.getAndIncrement() % 2;
if (counter.get() > 9999) { //當連線數超過9999時切換其他台Slave
counter.set(-1);
}
if (index == 0) {
set(DBTypeEnum.SLAVE);
} else {
set(DBTypeEnum.SLAVE);
}
}

/**
* remove 線程
*/
public static void remove(){
contextHolder.remove();
}
}

透過Spring AbstractRoutingDataSource 自動切換資料源,可以有很多個方法,
本身是使用 Spring AOP 及Mybatis-plus 的Plugin 做自動切換功能。為什麼不直接用Spring AOP就好,還需要使用Mybatis-Plus Plugin ,就是要確保在執行insert update , delete 時, 不使用到Slave 而是使用Master。
因本身AOP是直接攔截Service, 而不是 Mapper , 會遇到的問題是,當第一個執行為Select 時,Connection是連線Slave,在執行第二個 insert  or  update是不會切換成Master,因為是同一個線程,所以導致在 insert , update時會寫入Slave而不是Master ,後續才會用 Mybatis Plugin做補強。
@Aspect
@Component
@Slf4j
public class DataSourceAop {
/**
* select using slave
*/
@Pointcut("" +
"!@annotation(com.mybatis.dynamic.annotation.MybatisClusterMaster) " +
"&& " +
" (execution(* com.myProject.*.service..*.select*(..)) " +
"|| execution(* com.myProject.*.service..*.query*(..)) " +
"|| execution(* com.myProject.*.service..*.find*(..)) " +
"|| execution(* com.myProject.*.service..*.get*(..)) " +
")")
public void readPointcut() {
}

/**
* other using master
*/
@Pointcut("@annotation(com.mybatis.dynamic.annotation.MybatisClusterMaster) " +
"|| execution(* com.myProject.*.service..*.insert*(..)) " +
"|| execution(* com.myProject.*.service..*.add*(..)) " +
"|| execution(* com.myProject.*.service..*.save*(..)) " +
"|| execution(* com.myProject.*.service..*.update*(..)) " +
"|| execution(* com.myProject.*.service..*.edit*(..)) " +
"|| execution(* com.myProject.*.service..*.modify*(..)) " +
"|| execution(* com.myProject.*.service..*.delete*(..)) " +
"|| execution(* com.myProject.*.service..*.remove*(..))"
)
public void writePointcut() {
}

@Before("readPointcut()")
public void read() {
DBContextHolder.slave();
}

@Before("writePointcut()")
public void write() {
DBContextHolder.master();
}
}

創建 MybatisPluginInterceptor 
繼承了Interceptor ,攔截SQL在執行之前,判斷該SQL為 insert , update , delete 自動切換成master , 如果是select 不做轉換。
@Component
@Intercepts({
@Signature(type = Executor.class, method = "update", args = { MappedStatement.class , Object.class }),
@Signature(type = Executor.class, method = "query", args = { MappedStatement.class , Object.class , RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(type = Executor.class, method = "query", args = { MappedStatement.class , Object.class , RowBounds.class, ResultHandler.class }),

})
public class MybatisPluginInterceptor implements Interceptor {


/**
* 補仃 線程是一致性的,當第一次執行為select 無法切換成master , 透過攔截器自動切換
* @param invocation
* @return Object
*/
public Object intercept(Invocation invocation) throws Throwable {
Object[] objects = invocation.getArgs();
MappedStatement ms = (MappedStatement) objects[0];
boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
if(!synchronizationActive){
if( DBContextHolder.get().equals(DBTypeEnum.SLAVE) && ( ms.getSqlCommandType().equals(SqlCommandType.DELETE) || ms.getSqlCommandType().equals(SqlCommandType.UPDATE) || ms.getSqlCommandType().equals(SqlCommandType.INSERT) ) ){
DBContextHolder.master();
}
}
return invocation.proceed();
}

public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

public void setProperties(Properties properties) {
}
}


Github Code:


References