配置类封装-动态切换数据源
2022/9/18大约 2 分钟后端Spring框架封装
核心设计思路
动态数据源切换主要解决多租户+读写分离场景下的数据源路由问题。
1. 数据源路由 key
路由规则:{租户编码}_{MASTER|SLAVE}
CUS_001_MASTER // 租户CUS_001的主库
CUS_001_SLAVE // 租户CUS_001的从库
2. 核心组件
| 组件 | 职责 |
|---|---|
DynamicDataSourceContextHolder | 用 FastThreadLocal 存储当前线程的数据源类型 |
DynamicDataSource | 继承 AbstractRoutingDataSource,实现 determineCurrentLookupKey() 路由 |
DataSourceAspect | AOP 拦截 Mapper 调用,切换数据源 |
@DataSource | 注解,标注在类或方法上指定数据源 |
3. 数据源选择优先级
- 注解优先:
@DataSource标注在方法或类上 - 上下文其次:
DynamicDataSourceContextHolder中手动设置的 - SQL 判断:根据 SQL 是
SELECT走从库,其他走主库
4. 实现要点
DynamicDataSourceContextHolder
public class DynamicDataSourceContextHolder {
// 使用 FastThreadLocal 而非 ThreadLocal,性能更优
private static final FastThreadLocal<String> CONTEXT_HOLDER = new FastThreadLocal<>();
public static void setDataSourceType(String dsType) {
CONTEXT_HOLDER.set(dsType);
}
public static String getDataSourceType() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
DynamicDataSource 路由逻辑
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 拼接租户编码 + 数据源类型
return TenantUtils.getTenantCode() + "_" +
Optional.ofNullable(DynamicDataSourceContextHolder.getDataSourceType())
.orElse(DataSourceType.MASTER.name());
}
@Override
protected DataSource determineTargetDataSource() {
String lookupKey = (String) determineCurrentLookupKey();
Object dataSource = this.resolvedDataSources.get(lookupKey);
// 从库异常时降级到主库
if (dataSource == null) {
lookupKey = lookupKey.replace(DataSourceType.SLAVE.name(), DataSourceType.MASTER.name());
dataSource = this.resolvedDataSources.get(lookupKey);
}
return (DataSource) dataSource;
}
}
DataSourceAspect 拦截逻辑
@Aspect
@Component
public class DataSourceAspect {
@Around("execution(* com..*Mapper.*(..))")
public Object around(ProceedingJoinPoint point) throws Throwable {
// 1. 获取方法/类上的 @DataSource 注解
String dataSourceType = getDataSourceType(point);
// 2. 上下文中的数据源类型
if (StringUtils.isBlank(dataSourceType)) {
dataSourceType = DynamicDataSourceContextHolder.getDataSourceType();
}
// 3. 根据 SQL 自动判断
if (StringUtils.isBlank(dataSourceType)) {
String sql = getSql(point);
if (sql.toLowerCase().startsWith("select")) {
dataSourceType = DataSourceType.SLAVE.name();
} else {
dataSourceType = DataSourceType.MASTER.name();
}
}
// 4. 设置数据源
DynamicDataSourceContextHolder.setDataSourceType(dataSourceType);
try {
return point.proceed();
} finally {
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
}
@DataSource 注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
DataSourceType value() default DataSourceType.MASTER;
}
public enum DataSourceType {
MASTER, // 主库(写)
SLAVE // 从库(读)
}
5. 使用示例
// 在 Mapper 层标注走从库
@DataSource(DataSourceType.SLAVE)
public interface UserMapper {
List<User> selectUsers();
}
// 在 Service 层手动切换
public void someMethod() {
DynamicDataSourceContextHolder.setDataSourceType("SLAVE");
try {
// 查询走从库
userMapper.selectUsers();
} finally {
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
6. 关键设计点
- FastThreadLocal:比普通 ThreadLocal 性能更好
- 从库降级:从库连接异常时自动切换主库
- 读写计数:多从库时用
AtomicInteger轮询负载均衡 - 缓存映射:方法->数据源类型映射用
ConcurrentHashMap缓存,避免每次反射解析注解
