1. 程式人生 > >Spring+Mybatis透明實現讀寫分離

Spring+Mybatis透明實現讀寫分離

背景

網上有好多讀寫分離的實踐,所應對的業務場景也不一樣,本方法參考了網上好多方法,最終實現為快速應對中小型網際網路產品的讀寫分離。

資料庫環境:

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的配置。有機會我們在一起研究。
本文側重配置,描述偏少,見諒。如有問題,及時反饋。