为了提高数据库的查询效率,利用数据库主从机制,写走主库,查询走从库。如果只是实现一主一从类似简单的主从模式,可以继承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