多数据源动态切换
- 继承Spring-jbdc的
AbstractRoutingDataSource
,实现其determineCurrentLookupKey
方法来确定当前使用的数据源; - 一般主从切换针对方法级或类级,需要在执行方法或进入类之前明确当前需要的数据源,这时采用Aop的思想+注解来实现;
- 为了确保每个线程之间数据源的独立性,需要增加ThreadLocal来隔离。
1、继承AbstractRoutingDataSource
继承AbstractRoutingDataSource,实现其抽象方法determineCurrentLookupKey。
public class MSRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
//ThreadLocal记录当前线程使用的DataSource
return DataSourceHandler.getDataSource();
}
}
[外链图片转存失败(img-HP3SxgUl-1563977469384)(media/15633553276487/AbstractRoutingDataSource.png)]
AbstractRoutingDataSource实现jdk的
javax.sql.DataSource
接口,其DataSource创建Connection时会通过determineCurrentLookupKey方法来确定使用哪一个Datasource。
//1、创建数据库Connection
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
//2、确定使用哪一个数据源
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
//lookupKey从Map【resolvedDataSources】中获取对应的Datasource
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;
}
2、记录每个线程的DataSource
在Spring-jdbc确定使用哪一个数据源时,根据当前线程获取对应的DataSource。
public class DataSourceHandler {
private static ThreadLocal<String> handlerThreadLocal = new ThreadLocal<>();
/**
* 提供给AOP去设置当前的线程的数据源的信息
* @param dataSource
*/
public static void putDataSource(String dataSource) {
handlerThreadLocal.set(dataSource);
}
/**
* 提供给AbstractRoutingDataSource的实现类,通过key选择数据源
* @return
*/
public static String getDataSource() {
return handlerThreadLocal.get();
}
/**
* 清空, 使用默认数据源
*/
public static void remove() {
handlerThreadLocal.remove();
}
}
3、多数据源注解
@Target(value = {ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
/**
* 数据源名称
* @return
*/
String value() default "master";
}
- 使用哪一个数据源切面
//拦截方法或者类上的注解来确定使用哪一个Datasource
@Aspect
@Component
public class DataSourceHandlerAop {
private static final Logger log = LoggerFactory.getLogger(DataSourceHandlerAop.class);
//需要拦截的注解
@Pointcut("@within(com.xx.provider.datasource.DataSource) || @annotation(com.xx.provider.datasource.DataSource)")
public void pointcut() {
}
/**
* 在执行具体方法前 确定需要的数据源
*
* @param joinPoint
*/
@Before(value = "pointcut()")
public void doBefore(JoinPoint joinPoint) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//获取方法上的@DataSource注解
DataSource dataSourceAnnotation = method.getAnnotation(DataSource.class);
if (dataSourceAnnotation == null) {
//获取对应类上面的注解
dataSourceAnnotation = joinPoint.getTarget().getClass().getAnnotation(DataSource.class);
if(dataSourceAnnotation == null) {
return;
}
}
/**
* 获取指定的数据源 value,并将数据源保存到当前线程对应的ThreadLocal中,以便后续使用
*/
String dataSource = dataSourceAnnotation.value();
if (!StringUtils.isEmpty(dataSource)) {
DataSourceHandler.putDataSource(dataSource);
}
log.info("动态切换数据源,className: {}, methodName: {}, dataSource: {}", joinPoint.getTarget().getClass().getSimpleName(), method.getName(), dataSource);
}
/**
* 方法执行忘后,清空ThreadLocal,不能影响默认数据源
* @param joinPoint
*/
@After(value = "pointcut()")
public void doAfter(JoinPoint joinPoint) {
DataSourceHandler.remove();
}
}
- spring配置文件
<!--数据源,指定每个Datasoure的唯一标识,记在AbstractRoutingDataSource的Map中-->
<bean id="dataSource" class="com.xx.provider.datasource.MSRoutingDataSource" lazy-init="true">
<description>主备数据源</description>
<property name="targetDataSources">
<map key-type="java.lang.String" value-type="javax.sql.DataSource">
<entry key="master" value-ref="socialSecurityMasterDS"/>
<entry key="slave" value-ref="socialSecuritySlaveDS"/>
</map>
</property>
<!-- 设置默认的目标数据源 -->
<property name="defaultTargetDataSource" ref="socialSecurityMasterDS"/>
</bean>
如果有很多数据源,那也可以按照这个方式来指定某个方法使用哪一个DataSource。