1. 项目中使用多数据源的配置, 模拟一个场景, 配置手动的读写分离, 当方法前缀为 query get select find count 的方法为读请求, 就使用从数据库, 否则就使用主库. 但是如果我没有配置从库, 那么就使用默认的数据源 (主库).
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://127.0.0.1:3307/v3_admin_vite?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=true
url: jdbc:mysql://60.204.158.66:3306/v3_admin_vite?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=true
username: root
password: 123456
slave:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://127.0.0.1:3306/v3_admin_vite?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=true
username: root
password: 123456
2. 多数据源配置类 DataSourceConfig,主要是创建动态数据源, 事务管理器 和 SQL会话工厂, 使用了条件注解, 只有在配置了从库之后才被加载, 保证了未设置从库的情况会使用默认配置, 默认数据源.
@Configuration
@ConditionalOnProperty(prefix = "spring.datasource.slave", name = "jdbc-url")
@MapperScan(basePackages = "com.v3admin.mapper", sqlSessionFactoryRef = "sqlSessionFactory")
public class DataSourceConfig {
// 主库配置
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
// 从库配置(可选)
@Bean(name = "slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
try {
return DataSourceBuilder.create().build();
} catch (Exception e) {
// 如果从库配置不存在或无效,返回null
return null;
}
}
// 动态数据源配置
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource(
@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource);
// 只有当从库数据源有效时才添加
if (((HikariDataSource)slaveDataSource).getJdbcUrl() != null) {
targetDataSources.put("slave", slaveDataSource);
}
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(masterDataSource); // 默认使用主库
return dynamicDataSource;
}
// 配置 SqlSessionFactory
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/**/*.xml"));
return bean.getObject();
}
// 配置事务管理器
@Bean(name = "transactionManager")
public DataSourceTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
3. 使用了条件注解, 只有配置类被加载了,才去加载动态数据源类
@Component
@ConditionalOnBean(DataSourceConfig.class)
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSource(); // 从 ThreadLocal 获取当前数据源标识
}
/**
* 获取已解析的数据源映射
* @return 数据源映射
*/
public Map<Object, DataSource> getResolvedDataSources() {
return super.getResolvedDataSources();
}
}
将当前使用的数据源的 key 放入线程本地变量里面, 到时候动态数据源会自动获取
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSource(String dataSource) {
CONTEXT_HOLDER.set(dataSource);
}
public static String getDataSource() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}
4. AOP 类, 获取 get, select, find, query, count 开头的方法, 对它使用从库, 其他的默认是写操作, 使用主库!
也加了条件注解, @ConditionalOnBean(DataSourceConfig.class), 当配置类未加载, 这个 AOP 也不加载.
@Slf4j
@Aspect
@Component
@ConditionalOnBean(DataSourceConfig.class)
public class DynamicDataSourceAspect {
// 定义需要走从库的方法名前缀
private static final Set<String> SLAVE_PREFIXES = new HashSet<>(
Arrays.asList("get", "select", "find", "query", "count"));
private DynamicDataSource dynamicDataSource;
@Autowired
public void setDynamicDataSource(DynamicDataSource dynamicDataSource) {
this.dynamicDataSource = dynamicDataSource;
}
// 拦截 Service 层方法
@Pointcut("execution(* com.v3admin.service..*.*(..))")
public void serviceMethods() {}
// 环绕通知:根据方法名自动切换数据源
@Around("serviceMethods()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
// 判断方法名是否匹配从库前缀
boolean isSlaveMethod = SLAVE_PREFIXES.stream()
.anyMatch(methodName::startsWith);
try {
if (isSlaveMethod && hasSlaveDataSource()) {
DynamicDataSourceContextHolder.setDataSource("slave");
log.info("使用从库");
} else {
DynamicDataSourceContextHolder.setDataSource("master");
if (isSlaveMethod && !hasSlaveDataSource()) {
log.info("查询操作本应使用从库,但未配置从库,使用主库");
} else {
log.info("使用主库");
}
}
return joinPoint.proceed(); // 执行目标方法
} finally {
DynamicDataSourceContextHolder.clearDataSource(); // 清除数据源
}
}
/**
* 检查是否存在从库数据源
* @return 是否存在从库
*/
private boolean hasSlaveDataSource() {
Map<Object, DataSource> targetDataSources = dynamicDataSource.getResolvedDataSources();
return targetDataSources != null && targetDataSources.containsKey("slave");
}
}