Spring+Mybatis透明实现读写分离

16 篇文章 0 订阅

背景

网上有好多读写分离的实践,所应对的业务场景也不一样,本方法参考了网上好多方法,最终实现为快速应对中小型互联网产品的读写分离。

数据库环境:

1台master;多台slaver

适用框架:

spring+mybatis

操作数据库的简单原理:

mybatis最终是要通过sqlsessionfactory获取数据连接,创建sqlsession并提交到数据库的。所以我们入手的地方有两点:
1. 通过创建多种sqlsessionfactory比如masterFactory,slaverFactory来实现读写分离。
2. 让sqlsessionfactory直接可以动态获取到只读或者写的数据源。

解决方案:

通过扩展spring的AbstractRoutingDataSourceDataSourceTransactionManager来实现透明的读写分离。该类充当了DataSource的路由中介, 能有在运行时, 根据某种key值来动态切换到真正的DataSource上。

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    private Map<Object, Object> targetDataSources;
    private Object defaultTargetDataSource;
    private boolean lenientFallback = true;
    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
    private Map<Object, DataSource> resolvedDataSources;
    private DataSource resolvedDefaultDataSource;
    //我们可以看到,它继承了AbstractDataSource,而AbstractDataSource不就是javax.sql.DataSource的子类,
    //So我们可以分析下它的getConnection方法:
    @Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return determineTargetDataSource().getConnection(username, password);
    }
    /**
    上面这段源码的重点在于determineCurrentLookupKey()方法,这是AbstractRoutingDataSource类中的一个抽象方法,
    它的返回值是你所要用的数据源dataSource的key值,有了这个key值,
    resolvedDataSource(这是个map,由配置文件中设置好后存入的)就从中取出对应的DataSource,如果找不到,就用配置默认的数据源。
    **/
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        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;
    }
}

实现

现在我们需要做的就是重写determineTargetDataSource(),获取我们需要的数据源。

1 我们自己的动态数据源

public class DynamicDataSource extends AbstractRoutingDataSource {

    Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);

    private AtomicInteger counter = new AtomicInteger();

    private DataSource master;

    private List<DataSource> slaves;

    @Override
    protected Object determineCurrentLookupKey() {
        //do nothing
        return null;
    }

    @Override
    public void afterPropertiesSet() {
        //do nothing
    }

    /**
     * 根据标识获取数据源
     * @return
     */
    @Override
    protected DataSource determineTargetDataSource() {
        DataSource dataSource = null;
        if (DateSourceHolder.isMaster())
            dataSource = master;
        else if (DateSourceHolder.isSlave()){
            int count = counter.getAndIncrement();
            if (count > 1000000)
                counter.set(0);
            //简单轮循
            int sequence = count%slaves.size();
            dataSource = slaves.get(sequence);
        }else
            dataSource = master;

        //纯粹为了调试打印,线上需要注释掉
        /*
        if (dataSource instanceof org.apache.tomcat.jdbc.pool.DataSource){
            org.apache.tomcat.jdbc.pool.DataSource ds = (org.apache.tomcat.jdbc.pool.DataSource) dataSource;
            String jdbcUrl = ds.getUrl();
            int maxWait = ds.getMaxWait();
            logger.debug(">>>>>>>DataSource>>>>>>use jdbc maxWait :"+maxWait+"; url : "+jdbcUrl);
        }
        */

        return dataSource;
    }

    public DataSource getMaster() {
        return master;
    }

    public void setMaster(DataSource master) {
        this.master = master;
    }

    public List<DataSource> getSlaves() {
        return slaves;
    }

    public void setSlaves(List<DataSource> slaves) {
        this.slaves = slaves;
    }
}

2 数据源控制器

public class DateSourceHolder {

    private static final String MASTER = "master";
    private static final String SLAVE  = "slave";

    private static final ThreadLocal<String> dataSource = new ThreadLocal<>();
    private static final ThreadLocal<DataSource> masterLocal = new ThreadLocal<>();
    private static final ThreadLocal<DataSource> slaveLocal = new ThreadLocal<>();

    private static void setDataSource(String dataSourceKey){
        dataSource.set(dataSourceKey);
    }

    private  static String getDataSource(){
        return dataSource.get();
    }

    public static boolean isMaster(){
        return getDataSource() == MASTER;
    }

    public static boolean isSlave(){
        return getDataSource() == SLAVE;
    }

    public static void setSlave(DataSource dataSource){
        slaveLocal.set(dataSource);
    }

    public static void setMaster(DataSource dataSource){
        masterLocal.set(dataSource);
    }

    public static void setMaster(){
        setDataSource(MASTER);
    }

    public static void setSlave(){
        setDataSource(SLAVE);
    }

    public static void clearDataSource(){
        dataSource.remove();
        masterLocal.remove();
        slaveLocal.remove();
    }

}

3 扩展事务处理器

public class DynamicDataSourceTransactionManager extends DataSourceTransactionManager {

    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        boolean readOnly = definition.isReadOnly();//获取当前事务切点的方法的读写属性(在spring的xml或者事务注解中的配置)
        if (readOnly)
            DateSourceHolder.setSlave();
        else
            DateSourceHolder.setMaster();
        super.doBegin(transaction, definition);
    }

    @Override
    protected void doCleanupAfterCompletion(Object transaction) {
        super.doCleanupAfterCompletion(transaction);
        DateSourceHolder.clearDataSource();
    }
}

4 配置文件(部分)

<!-- 读取配置文件等等扫描  略...-->
<!--abstract数据源配置 -->
    <bean id="abstractDataSource" abstract="true">
        <property name="driverClassName" value="${jdbc.driverClassName}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />
    </bean>

  <!--tomcat jdbc pool数据源配置 -->
    <bean id="dataSourceMaster" name="dataSourceMaster" class="org.apache.tomcat.jdbc.pool.DataSource"
        destroy-method="close">
        <property name="poolProperties">
            <bean class="org.apache.tomcat.jdbc.pool.PoolProperties" parent="abstractDataSource">
                <property name="url" value="${jdbc.url}" />
                <property name="maxWait" value="${tomcat.jdbc.maxWait}" /> <!-- 调试读写用,线上不需要-->
            </bean>
        </property>
    </bean>
    <bean id="dataSourceSlaver1" name="dataSourceSlaver1" class="org.apache.tomcat.jdbc.pool.DataSource"
          destroy-method="close">
        <property name="poolProperties">
            <bean class="org.apache.tomcat.jdbc.pool.PoolProperties" parent="abstractDataSource">
                <property name="url" value="${jdbc.slaver1.url}" />
                <property name="maxWait" value="${tomcat.jdbc.slaver1.maxWait}" /><!-- 调试读写用,线上不需要-->
            </bean>
        </property>
    </bean>
    <bean id="dataSourceSlaver2" name="dataSourceSlaver2" class="org.apache.tomcat.jdbc.pool.DataSource"
          destroy-method="close">
        <property name="poolProperties">
            <bean class="org.apache.tomcat.jdbc.pool.PoolProperties" parent="abstractDataSource">
                <property name="url" value="${jdbc.slaver2.url}" />
                <property name="maxWait" value="${tomcat.jdbc.slaver2.maxWait}" />
            </bean>
        </property>
    </bean>

    <bean id="dataSource" class="cn.com.demo.dao.core.DynamicDataSource">
        <property name="master" ref="dataSourceMaster"></property>
        <property name="slaves">
            <list>
                <ref bean="dataSourceSlaver1"></ref>
                <ref bean="dataSourceSlaver2"></ref>
            </list>
        </property>
    </bean>
  <!-- mybatis -->
  <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="configLocation" value="classpath:mybatis-config.xml"></property>
    <property name="mapperLocations" >
        <value>classpath*:cn/com/demo/dao/mapper/xml/**/*Mapper.xml</value>
    </property>
    <property name="typeAliasesPackage" value="cn.com.demo.dao.entity" />
    <property name="plugins">
       <array>
           <bean class="com.github.pagehelper.PageHelper">
               <property name="properties">
                   <value>
                       dialect=mysql
                       offsetAsPageNum=true
                       rowBoundsWithCount=true
                       pageSizeZero=true
                       reasonable=true
                   </value>
               </property>
           </bean>
       </array>
   </property>
  </bean>
  <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
    <property name="basePackage" value="cn.com.demo.dao.mapper" />
  </bean>

    <!-- 事务管理器 -->
    <bean id="transactionManager"
    class="cn.com.demo.dao.core.DynamicDataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
  </bean>
    <!-- 声明式事务 -->
  <tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="query*" read-only="true" /> <!-- 在扩展的事务管理器中就可以获取到readOnly属性了 -->
        <tx:method name="get*" read-only="true" />
        <tx:method name="select*" read-only="true" />
        <tx:method name="*" propagation="REQUIRED" rollback-for="Exception"/>
    </tx:attributes>
  </tx:advice>
    <aop:config>
    <aop:pointcut expression="execution(* cn.com.demo.service..*(..))"
      id="ops" />
    <aop:advisor advice-ref="txAdvice" pointcut-ref="ops" />
  </aop:config>

5 应用

public interface ProductService {
    //匹配到*事务切点,会用master数据源
    Long addProduct(ProductAddDTO productAddDTO,CompanyUser com);
    /**
    简单的运行流程:
    1.匹配到query*事务切点,我们扩展的事务管理器会获取到read-only="true"属性
    2. 调用DateSourceHolder.setSlave();
    3. SQLSessionFactory会调用determineTargetDataSource()方法获取数据源,
        在方法中通过 if (DateSourceHolder.isSlave())  dataSource = slaves.get(sequence);
        将datasource设置为只读数据源。
    4. 最终service调用mapper操作数据库,mapper执行的时候用到的sqlSession中的connection即动态获取的只读数据源。
    **/
    Map<String,Object> queryUserGroups(Long id);
}

至此,读写分离就配置完了,还是开头说的,这种配置只使用1+N的中小项目中。大型项目可能需要N+N的配置。有机会我们在一起研究。
本文侧重配置,描述偏少,见谅。如有问题,及时反馈。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值