為了提高資料庫的查詢效率,利用資料庫主從機制,寫走主庫,查詢走從庫。如果隻是實作一主一從類似簡單的主從模式,可以繼承AbstractRoutingDataSource實作讀寫分離。而不需使用mycat,sharedingJDBC等資料庫插件。
分析AbstractRoutingDataSource可知,defaultTargetDataSource,表示預設的資料源;targetDataSources表示配置的所有資料源集合;afterPropertiesSet方法spring bean對象初始化方法,會把targetDataSources和defaultTargetDataSource,設定為resolvedDataSources和resolvedDefaultDataSource。getConnection()擷取jdbc的連接配接,并通過determineTargetDataSource()擷取指定的資料源,AbstractRoutingDataSource使用模闆類的模式,在父類定義了determineCurrentLookupKey()虛拟方法,擷取lookupkey對象;其子類必須實作該方法。源碼如下:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
/**
*配置的資料源
*/
@Nullable
private Map<Object, Object> targetDataSources;
/**
*預設資料源
*/
@Nullable
private Object defaultTargetDataSource;
......
/**
*spring InitializingBean 實作方法,bean初始化時調用
*/
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
/**
*擷取jdbc連結時,調用determineTargetDataSource,擷取指定的資料
*/
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
/**
*determineCurrentLookupKey方法通過子類自定義實作,擷取lookupKey,然後從resolvedDefaultDataSource map對象中擷取資料源
*/
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
/**
*子類必須實作的擷取lookupKey的方法
*/
protected abstract Object determineCurrentLookupKey();
}
建立DataSourceAddressEnum枚舉類,定義MASTER與SLAVE,路由名稱。代碼如下:
public enum DataSourceAddressEnum {
/**
* 主資料庫
*/
MASTER,
/**
* 從資料庫
*/
SLAVE;
}
建立DataSourceContextHolder,使用ThreadLocal,定義每次操作的類型枚舉,代碼如下:
public class DataSourceContextHolder {
private static final ThreadLocal<DataSourceAddressEnum> CONTEXT_HOLDER = ThreadLocal.withInitial(() -> DataSourceAddressEnum.MASTER);
public static void setCurrentDataSource(DataSourceAddressEnum dataSourceAddressEnum) {
CONTEXT_HOLDER.set(dataSourceAddressEnum);
}
public static DataSourceAddressEnum getCurrentDataSource() {
return CONTEXT_HOLDER.get();
}
public static void removeDataSource() {
CONTEXT_HOLDER.remove();
}
}
建立RoutingDataSourceWithAddress,繼承AbstractRoutingDataSource,實作determineCurrentLookupKey,即實作了可以根據DataSourceAddressEnum枚舉類實作資料源的動态路由,代碼如下:
public class RoutingDataSourceWithAddress extends AbstractRoutingDataSource {
/**
* @param defaultTargetDataSource 預設的 DataSource
* @param targetDataSources 配置的所有 DataSource
*/
public RoutingDataSourceWithAddress(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
}
/**
*配置的資料源
*/
@Override
protected Object determineCurrentLookupKey() {
DataSourceAddressEnum routingDataSourceAddressEnum = DataSourceContextHolder.getCurrentDataSource();
if (log.isDebugEnabled()) {
log.debug("routing data source address is {}", routingDataSourceAddressEnum.name());
}
return routingDataSourceAddressEnum;
}
}
使用AOP+注解的方式,對指定的方法進行資料源動态切換的控制。建立RoutingDataSource注解,定義需要路由的資料源,建立RoutingDataSourceAOP定義資料源路由的切面操作,代碼如下:
/**
* DataSource路由注解
**/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RoutingDataSource {
/**
* 路由的DataSource位址,預設為MASTER
*/
DataSourceAddressEnum value() default DataSourceAddressEnum.MASTER;
}
/**
* RoutingDataSource 的aop攔截
**/
@Aspect
@Component
@Order(10000)
@Slf4j
public class RoutingDataSourceAOP {
@Pointcut("@annotation(com.kuqi.mall.demo.conmon.datasource.RoutingDataSource)|| @within(com.kuqi.mall.demo.conmon.datasource.RoutingDataSource)")
public void routingDataSourcePointcut() {
}
@Around("routingDataSourcePointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RoutingDataSource routerDataSource = method.getAnnotation(RoutingDataSource.class);
// 如果沒有設定則預設為 MASTER
DataSourceAddressEnum dataSourceAddressEnum = Objects.isNull(routerDataSource) ? DataSourceAddressEnum.MASTER : routerDataSource.value();
DataSourceContextHolder.setCurrentDataSource(dataSourceAddressEnum);
try {
return joinPoint.proceed();
} finally {
DataSourceContextHolder.removeDataSource();
}
}
}
建立基于Springboot的自動配置類RoutingDataSourceAutoConfiguration,隻要配置了master和slave的屬性檔案,和mybatis屬性檔案,就可以自動啟動配置。RoutingDataSourceAutoConfiguration源碼如下:
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class, DruidDataSource.class})
@EnableConfigurationProperties(MybatisProperties.class)
public class RoutingDataSourceAutoConfiguration {
/**
* 配置master資料源
*/
@Bean(name = "masterDataSource", initMethod = "init", destroyMethod = "close")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.druid.master")
public DataSource masterDataSource() {
DataSource dataSource = DataSourceBuilder.create(this.getClass().getClassLoader())
.type(com.alibaba.druid.pool.DruidDataSource.class).build();
return dataSource;
}
/**
* 配置slave資料源
*/
@Bean(name = "slaveDataSource", initMethod = "init", destroyMethod = "close")
@ConfigurationProperties(prefix = "spring.datasource.druid.slave")
public DataSource slaveDataSource() {
DataSource dataSource = DataSourceBuilder.create(this.getClass().getClassLoader())
.type(com.alibaba.druid.pool.DruidDataSource.class).build();
return dataSource;
}
/**
* 初始化路由DataSource
*/
@Bean(name = "routingDataSourceWithAddress")
public DataSource dataSource(
@Autowired @Qualifier("masterDataSource") DataSource masterDataSource,
@Autowired @Qualifier("slaveDataSource") DataSource slaveDataSource) {
DataSource defaultTargetDataSource;
Map<Object, Object> targetDataSources = ImmutableMap.of(
DataSourceAddressEnum.MASTER, defaultTargetDataSource = masterDataSource,
DataSourceAddressEnum.SLAVE, slaveDataSource);
return new RoutingDataSourceWithAddress(defaultTargetDataSource, targetDataSources);
}
/**
* 使用SqlSessionFactoryBean配置MyBatis的SqlSessionFactory
**/
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(
@Autowired @Qualifier("routingDataSourceWithAddress") DataSource routingDataSourceWithAddress,
@Autowired MybatisProperties mybatisProperties,
@Autowired ResourceLoader resourceLoader) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(routingDataSourceWithAddress);
// 設定configuration
org.apache.ibatis.session.Configuration configuration = mybatisProperties.getConfiguration();
factory.setConfiguration(configuration);
// 設定SqlSessionFactory屬性
String configLocation;
if (StringUtils.isNotBlank(configLocation = mybatisProperties.getConfigLocation())) {
factory.setConfigLocation(resourceLoader.getResource(configLocation));
}
Resource[] resolveMapperLocations;
if (ArrayUtils.isNotEmpty(resolveMapperLocations = mybatisProperties.resolveMapperLocations())) {
factory.setMapperLocations(resolveMapperLocations);
}
String typeHandlersPackage;
if (StringUtils.isNotBlank(typeHandlersPackage = mybatisProperties.getTypeHandlersPackage())) {
factory.setTypeHandlersPackage(typeHandlersPackage);
}
String typeAliasesPackage;
if (StringUtils.isNotBlank(typeAliasesPackage = mybatisProperties.getTypeAliasesPackage())) {
factory.setTypeAliasesPackage(typeAliasesPackage);
}
return factory.getObject();
}
/**
* 使用routingDataSourceWithAddress配置資料庫事務
*/
@Bean
@ConditionalOnMissingBean
public DataSourceTransactionManager dataSourceTransactionManager(
@Autowired @Qualifier("routingDataSourceWithAddress") DataSource routingDataSourceWithAddress) {
return new DataSourceTransactionManager(routingDataSourceWithAddress);
}
/**
* 程式設計式事務
*/
@Bean
public TransactionTemplate transactionTemplate(
@Autowired @Qualifier("dataSourceTransactionManager") PlatformTransactionManager platformTransactionManager) {
return new TransactionTemplate(platformTransactionManager);
}
@Bean
@ConditionalOnMissingBean(RoutingDataSourceAOP.class)
public RoutingDataSourceAOP rRoutingDataSourceAOP() {
return new RoutingDataSourceAOP();
}
}
資料源使用DruidDataSource,詳細的配置檔案,master和slave資料源,可以配置不同的資料庫用來測試,實際開發中,配置為滿足資料庫主從複制的配置,配置代碼如下:
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
master:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/kuqi_mall?autoReconnectForPools=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
slave:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/kuqi_mall?autoReconnectForPools=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
在springboot的啟動類中,屏蔽DruidDataSourceAutoConfigure自動配置類,就能啟動。啟動類代碼,以及操作示例代碼如下:
/**
* springboot啟動類exclude DruidDataSourceAutoConfigure
*/
@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)
@MapperScan(basePackages = {"com.kuqi.mall.demo.dao"})
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
/**
* 注解定義動态資料源操作
*/
@Override
@RoutingDataSource(DataSourceAddressEnum.SLAVE)
public CouponBo getFromSlave(Long id) {
return get(id);
}
完整示例代碼,可以參考連結spring-boot-mall項目https://github.com/alldays/spring-boot-mall