Spring+MyBatis实现数据库读写分离方案4(在我自己的项目中的实际应用)

一、网上好多地方都写了spring+mybatis读写分离的四种方案,本人亲自在项目中应用了方案4,不过网上说的方案4存在问题,查询也走了主库。本文中该问题已修正。

一、自定义动态数据源(继承AbstractRoutingDataSource)实现读写分离

import org.apache.commons.collections.CollectionUtils;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created by IDEA
 * User: zyj
 * Date: 2018-06-26 10:56
 * Desc: 动态数据源实现读写分离
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    private Object writeDataSource; //写数据源

    private List<Object> readDataSources; //多个读数据源

    private int readDataSourcePollPattern = 0; //获取读数据源方式,0:随机,1:轮询

    private AtomicLong counter = new AtomicLong(0);

    private static final Long MAX_POOL = Long.MAX_VALUE;

    private final Lock lock = new ReentrantLock();


    @Override
    public void afterPropertiesSet() {
        if (this.writeDataSource == null) {
            throw new IllegalArgumentException("Property 'writeDataSource' is required");
        }
        setDefaultTargetDataSource(writeDataSource);
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DynamicDataSourceGlobal.WRITE.name(), writeDataSource);
        if(CollectionUtils.isNotEmpty(readDataSources)) {
            for(int i=0; i<readDataSources.size(); i++){
                targetDataSources.put(DynamicDataSourceGlobal.READ.name()+i, readDataSources.get(i));
            }
        }
        setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {

        DynamicDataSourceGlobal dynamicDataSourceGlobal = DynamicDataSourceHolder.getDataSource();

        if(dynamicDataSourceGlobal == null
                || dynamicDataSourceGlobal == DynamicDataSourceGlobal.WRITE) {
            return DynamicDataSourceGlobal.WRITE.name();
        }

        int index = 0;

        int readDataSourceSize =  readDataSources.size();

        if(readDataSourcePollPattern == 1) {
            //轮询方式
            long currValue = counter.incrementAndGet();
            if((currValue + 1) >= MAX_POOL) {
                try {
                    lock.lock();
                    if((currValue + 1) >= MAX_POOL) {
                        counter.set(0);
                    }
                } finally {
                    lock.unlock();
                }
            }
            index = (int) (currValue % readDataSourceSize);
        } else {
            //随机方式
            index = ThreadLocalRandom.current().nextInt(0, readDataSourceSize);
        }
        return DynamicDataSourceGlobal.READ.name() + index;
    }

    public void setWriteDataSource(Object writeDataSource) {
        this.writeDataSource = writeDataSource;
    }

    public Object getWriteDataSource() {
        return writeDataSource;
    }

    public List<Object> getReadDataSources() {
        return readDataSources;
    }

    public void setReadDataSources(List<Object> readDataSources) {
        this.readDataSources = readDataSources;
    }

    public int getReadDataSourcePollPattern() {
        return readDataSourcePollPattern;
    }

    public void setReadDataSourcePollPattern(int readDataSourcePollPattern) {
        this.readDataSourcePollPattern = readDataSourcePollPattern;
    }
}

该类中引用到两个类,很简单,也贴出来

/**
 * Created by IDEA
 * User: zyj
 * Date: 2018-06-26 10:56
 * Desc:
 */
public enum DynamicDataSourceGlobal {
    READ, WRITE;
}
public final class DynamicDataSourceHolder {

    private static final ThreadLocal<DynamicDataSourceGlobal> holder = new ThreadLocal<DynamicDataSourceGlobal>();

    private DynamicDataSourceHolder() {
        //
    }

    public static void putDataSource(DynamicDataSourceGlobal dataSource){
        holder.set(dataSource);
    }

    public static DynamicDataSourceGlobal getDataSource(){
        return holder.get();
    }

    public static void clearDataSource() {
        holder.remove();
    }

}

动态数据源事务管理类

import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;

/**
 * Created by IDEA
 * User: zyj
 * Date: 2018-06-26 10:56
 * Desc:
 */
public class DynamicDataSourceTransactionManager extends DataSourceTransactionManager {

    /**
     * 只读事务到读库,读写事务到写库
     * @param transaction
     * @param definition
     */
    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition) {

        //设置数据源
        boolean readOnly = definition.isReadOnly();
        if(readOnly) {
            DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobal.READ);
        } else {
            DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobal.WRITE);
        }
        super.doBegin(transaction, definition);
    }

    /**
     * 清理本地线程的数据源
     * @param transaction
     */
    @Override
    protected void doCleanupAfterCompletion(Object transaction) {
        super.doCleanupAfterCompletion(transaction);
        DynamicDataSourceHolder.clearDataSource();
    }
}

最后是mybatis的自定义拦截器

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Created by IDEA
 * User: zyj
 * Date: 2018-06-26 10:56
 * Desc:
 */
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {
        MappedStatement.class, Object.class }),
@Signature(type = Executor.class, method = "query", args = {
        MappedStatement.class, Object.class, RowBounds.class,
        ResultHandler.class }) })
public class DynamicPlugin implements Interceptor {

    protected static final Logger logger = LoggerFactory.getLogger(DynamicPlugin.class);

    private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*";

    private static final Map<String, DynamicDataSourceGlobal> cacheMap = new ConcurrentHashMap<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

//        boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive(); //不能用这个判断,网上的方案4都错在了这里。
        boolean actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive();
        if(!actualTransactionActive) {
            Object[] objects = invocation.getArgs();
            MappedStatement ms = (MappedStatement) objects[0];

            DynamicDataSourceGlobal dynamicDataSourceGlobal = null;

            if((dynamicDataSourceGlobal = cacheMap.get(ms.getId())) == null) {
                //读方法
                if(ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
                    //!selectKey 为自增id查询主键(SELECT LAST_INSERT_ID() )方法,使用主库
                    if(ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
                        dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
                    } else {
                        BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
                        String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", " ");
                        if(sql.matches(REGEX)) {
                            dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
                        } else {
                            dynamicDataSourceGlobal = DynamicDataSourceGlobal.READ;
                        }
                    }
                }else{
                    dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
                }
                logger.info("设置方法[{}] use [{}] Strategy, SqlCommandType [{}]..", ms.getId(), dynamicDataSourceGlobal.name(), ms.getSqlCommandType().name());
                cacheMap.put(ms.getId(), dynamicDataSourceGlobal);
            }
            DynamicDataSourceHolder.putDataSource(dynamicDataSourceGlobal);
        }

        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {
        //
    }
}


至此,要引入的类都OK了。接下来说说如何在项目中引用这些类。

扫描二维码关注公众号,回复: 2253239 查看本文章

首先DynamicDataSource肯定是在配置数据源的地方。

<!-- dataSource 配置 -->
<bean id="dataSource" class="cn.remotejob.readwrite.DynamicDataSource">
   <property name="writeDataSource" ref="dataSourceWrite" />
   <property name="readDataSources">
      <list>
         <ref bean="dataSourceRead1" />
         <ref bean="dataSourceRead2" />
      </list>
   </property>
   <!--获取读数据源方式,0:随机,1:轮询-->
   <property name="readDataSourcePollPattern" value="1" />
</bean>

   <bean id="dataSourceWrite" class="org.apache.commons.dbcp.BasicDataSource"
   destroy-method="close">
   <property name="driverClassName" value="com.mysql.jdbc.Driver" />
   <property name="url" value="${jdbc.url}" />
   <property name="username" value="${jdbc.username}" />
   <property name="password" value="${jdbc.password}" />
   <property name="validationQuery" value="SELECT 1" />
       <property name="testOnBorrow" value="true"/>
       <!-- 超时等待时间以毫秒为单位 -->
       <property name="maxWait" value="3000"/>
   <property name="connectionInitSqls" value="set names utf8mb4;"/>
</bean>
<bean id="dataSourceRead1" class="org.apache.commons.dbcp.BasicDataSource"
     destroy-method="close">
   <property name="driverClassName" value="com.mysql.jdbc.Driver" />
   <property name="url" value="${jdbc.url_1}" />
   <property name="username" value="${jdbc.username_1}" />
   <property name="password" value="${jdbc.password_1}" />
   <property name="validationQuery" value="SELECT 1" />
   <property name="testOnBorrow" value="true"/>
   <!-- 超时等待时间以毫秒为单位 -->
   <property name="maxWait" value="3000"/>
   <property name="connectionInitSqls" value="set names utf8mb4;"/>
</bean>
<bean id="dataSourceRead2" class="org.apache.commons.dbcp.BasicDataSource"
     destroy-method="close">
   <property name="driverClassName" value="com.mysql.jdbc.Driver" />
   <property name="url" value="${jdbc.url_2}" />
   <property name="username" value="${jdbc.username_2}" />
   <property name="password" value="${jdbc.password_2}" />
   <property name="validationQuery" value="SELECT 1" />
   <property name="testOnBorrow" value="true"/>
   <!-- 超时等待时间以毫秒为单位 -->
   <property name="maxWait" value="3000"/>
   <property name="connectionInitSqls" value="set names utf8mb4;"/>
</bean>
 

其次DynamicPlugin作为插件需要配置在下面的地方

<!-- mybatis文件配置,扫描所有mapper文件 -->  
   <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" 
    p:dataSource-ref="dataSource" 
    p:mapperLocations="classpath:cn/remotejob/dao/*.xml">
   <property name="plugins">
      <array>
         <bean class="cn.remotejob.readwrite.DynamicPlugin" />
      </array>
   </property>
</bean>

最后DynamicDataSourceTransactionManager当然是自定义事务管理器,代替org.springframework.jdbc.datasource.DataSourceTransactionManager,因为DynamicDataSourceTransactionManager继承了它。

<bean id="transactionManager" class="cn.remotejob.readwrite.DynamicDataSourceTransactionManager" p:dataSource-ref="dataSource" />

到此,读写分离已经实现了!

但如果我刚写的数据想要立刻查出来,就必须让这个查询也走主库。要解决这个问题,可以让这个写和读放到一个方法中,然后在方法上加上事务@Transactional,这样就可以走主库了。当然,配置文件中要启用对事务注解的支持,<tx:annotation-driven transaction-manager="transactionManager" />


猜你喜欢

转载自blog.csdn.net/keketrtr/article/details/80816235
今日推荐