spring下的数据库主从分离(上)

基于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


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。