基于mysql数据库已经做好了主从,提供出主库和从库的链接
1、实现方式
主要思路是重写spring的AbstractRoutingDataSource类,使用ThreadLocal保存数据源信息,使用aop切service的方法动态切换数据源。
先看下配置文件
<bean id="dataSource" class="com.storm.common.datasource.MyDataSource">
<property name="targetDataSources">
<map>
<entry key="master" value-ref="dataSourceW"></entry>
<entry key="slave" value-ref="dataSourceR"></entry>
</map>
</property>
<property name="defaultTargetDataSource" ref="dataSourceW" />
</bean>
MyDataSource是我们重写AbstractRoutingDataSource的determineCurrentLookupKey()方法的类
public class MyDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceHolder.getRoutingKey();
}
}
dataSource这个bean里面的dataSourceW、dataSourceR是我们配置文件上面配置出的两个数据源的bean。targetDataSources用于运行时通过key获取具体数据源,defaultTargetDataSource必须配置,用于默认或者某些情况下使用,下面要说的事务的获取数据源就依赖这个。然后将该dataSource作为参数用于初始化事务DataSourceTransactionManager和SqlSessionFactoryBean。
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="typeAliasesPackage" value="com.storm.entity"/>
<property name="mapperLocations" value="classpath:datasource/mapper/*.xml"/>
<property name="configuration">
<bean class="org.apache.ibatis.session.Configuration">
<!-- 打印sql -->
<!--<property name="logImpl" value="org.apache.ibatis.logging.stdout.StdOutImpl"/>-->
</bean>
</property>
</bean>
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate" primary="true">
<constructor-arg index="0" ref="sqlSessionFactory"/>
</bean>
<!-- 开启Dao接口自动实现 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.storm.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
<!-- Spring事务管理 -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<constructor-arg ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="txManager"/>
然后是保存数据源的ThreadLocal类
public class DataSourceHolder {
private static final ThreadLocal<String> routingKey = new ThreadLocal<>();
public static void setRoutingKey(String key) {
routingKey.set(key);
}
public static String getRoutingKey() {
return routingKey.get();
}
public static void removeRoutingKey() {
routingKey.remove();
}
}
有set和get方法用于存放、获取数据源key,remove这个方法也很重要,涉及到后面提到的一个坑。
然后定义一个切点,使用aop动态切入数据源
<aop:aspectj-autoproxy proxy-target-class="true"/>
<bean id="dataSourceAspect" class="com.storm.common.datasource.DataSourceAspect" />
<aop:config>
<aop:aspect id="c" ref="dataSourceAspect">
<aop:pointcut id="tx" expression="execution(* com.storm.service..*.*(..))"/>
<aop:around pointcut-ref="tx" method="around"/>
</aop:aspect>
</aop:config>
这里是切的service包及其子包下的所有类,然后就是重要的切面类了
public class DataSourceAspect {
private static final Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);
public Object around(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method targetMethod = signature.getMethod();
DataSource dataSource = targetMethod.getAnnotation(DataSource.class);
logger.debug("DataSourceAspect setDataSource targetMethod={}, dataSource={}", targetMethod, dataSource);
if (dataSource == null) {
//兼容之前代码,不设置标签的就会被设置为master。
DynamicDataSourceHolder.putDataSource(DataSourceConstant.MASTER);
} else {
if (dataSource.value().equals(DataSourceConstant.MASTER)) {
DynamicDataSourceHolder.putDataSource(DataSourceConstant.MASTER);
} else if (dataSource.value().equals(DataSourceConstant.SLAVE)) {
DynamicDataSourceHolder.putDataSource(DataSourceConstant.SLAVE);
} else {
DynamicDataSourceHolder.putDataSource(DataSourceConstant.MASTER);
}
}
Object result = null;
try {
result = pjp.proceed();
} catch (Throwable e) {
throw e;
} finally {
//在处理完切点代码后将ThreadLocal值清掉,线程池复用线程会保留上一个线程设置的值,造成混乱
//实际上service多层调用时,里层调用结束时就会将线程数据源清空了,回到外层数据源变成默认数据源,所以将一定要走从数据源的单写个方法
DynamicDataSourceHolder.remove();
}
return result;
}
}
拿到方法的标注数据源的标签后,做一些处理,由于项目一开始没有做主从分离,所以策略上很谨慎,尽量不出问题的配置。
1、如果数据源标签为空没设置的时候,使用写数据源,这样保证安全。
2、使用around代替before切面,因为我们需要在方法结束后将ThreadLocal里面的数据源信息清空,这也是个坑,网上的方法大多数是没有这个步骤的,但是不知道是否还有其他好的办法,可以在评论里面提出。
在相应的service方法上注明数据源,debug看一下就可以使用了
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSource {
String value() default DataSourceConstant.MASTER;
}
@Override
@DataSource(DataSourceConstant.SLAVE)
public List<User> select() {
return userDAO.select();
}
加在service的实现层即可
到这使用配置方面就ok了,下篇介绍下源码和坑。
https://www.jianshu.com/p/f5a234e21d2a