Spring+MyBatis实现数据库读写分离方案

方案1
通过MyBatis配置文件创建读写分离两个DataSource,每个SqlSessionFactoryBean对象的mapperLocations属性制定两个读写数据源的配置文件。将所有读的操作配置在读文件中,所有写的操作配置在写文件中。

优点:实现简单
缺点:维护麻烦,需要对原有的xml文件进行重新修改,不支持多读,不易扩展
实现方式

<bean id="abstractDataSource" abstract="true" class="com.alibaba.druid.pool.DruidDataSource" init-method="init"
destroy-method="close">
<property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="60000"/>
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="300000"/>
<property name="validationQuery" value="SELECT ‘x‘"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
<!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
<property name="poolPreparedStatements" value="true"/>
<property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>
<property name="filters" value="config"/>
<property name="connectionProperties" value="config.decrypt=true" />
</bean>

<bean id="readDataSource" parent="abstractDataSource">
<!-- 基本属性 url、user、password -->
<property name="url" value="${read.jdbc.url}"/>
<property name="username" value="${read.jdbc.user}"/>
<property name="password" value="${read.jdbc.password}"/>
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${read.jdbc.initPoolSize}"/>
<property name="minIdle" value="10"/>
<property name="maxActive" value="${read.jdbc.maxPoolSize}"/>
</bean>

<bean id="writeDataSource" parent="abstractDataSource">
<!-- 基本属性 url、user、password -->
<property name="url" value="${write.jdbc.url}"/>
<property name="username" value="${write.jdbc.user}"/>
<property name="password" value="${write.jdbc.password}"/>
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${write.jdbc.initPoolSize}"/>
<property name="minIdle" value="10"/>
<property name="maxActive" value="${write.jdbc.maxPoolSize}"/>
</bean>

<bean id="readSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 实例化sqlSessionFactory时需要使用上述配置好的数据源以及SQL映射文件 -->
<property name="dataSource" ref="readDataSource"/>
<property name="mapperLocations" value="classpath:mapper/read/*.xml"/>
</bean>

<bean id="writeSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 实例化sqlSessionFactory时需要使用上述配置好的数据源以及SQL映射文件 -->
<property name="dataSource" ref="writeDataSource"/>
<property name="mapperLocations" value="classpath:mapper/write/*.xml"/>
</bean>

方案2
通过Spring AOP在业务层实现读写分离,在DAO层调用前定义切面,利用Spring的AbstractRoutingDataSource解决多数据源的问题,实现动态选择数据源

优点:通过注解的方法在DAO每个方法上配置数据源,原有代码改动量少,易扩展,支持多读
缺点:需要在DAO每个方法上配置注解,人工管理,容易出错
实现方式

//定义枚举类型,读写
public enum DynamicDataSourceGlobal {
READ, WRITE;
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**

  • RUNTIME
  • 定义注解
  • 编译器将把注释记录在类文件中,在运行时 VM 将保留注释,因此可以反射性地读取。
  • @author shma1664
  • */br/>@Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface DataSource {

    public DynamicDataSourceGlobal value() default DynamicDataSourceGlobal.READ;

}
/**

  • Created by IDEA
  • 本地线程设置和获取数据源信息
  • User: mashaohua
  • Date: 2016-07-07 13:35
  • Desc:
    */
    public class DynamicDataSourceHolder {

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

    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.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: mashaohua
  • Date: 2016-07-14 10:56
  • Desc: 动态数据源实现读写分离
    */
    public class DynamicDataSource extends AbstractRoutingDataSource {

    private Object writeDataSource; //写数据源

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

    private int readDataSourceSize; //读数据源个数

    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 (this.readDataSources == null) {
    readDataSourceSize = 0;
    } else {
    for(int i=0; i<readDataSources.size(); i++) {
    targetDataSources.put(DynamicDataSourceGlobal.READ.name() + i, readDataSources.get(i));
    }
    readDataSourceSize = readDataSources.size();
    }
    setTargetDataSources(targetDataSources);
    super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {

    DynamicDataSourceGlobal dynamicDataSourceGlobal = DynamicDataSourceHolder.getDataSource();
    
    if(dynamicDataSourceGlobal == null
            || dynamicDataSourceGlobal == DynamicDataSourceGlobal.WRITE
            || readDataSourceSize &lt;= 0) {
        return DynamicDataSourceGlobal.WRITE.name();
    }
    
    int index = 1;
    
    if(readDataSourcePollPattern == 1) {
        //轮询方式
        long currValue = counter.incrementAndGet();
        if((currValue + 1) &gt;= MAX_POOL) {
            try {
                lock.lock();
                if((currValue + 1) &gt;= MAX_POOL) {
                    counter.set(0);
                }
            } finally {
                lock.unlock();
            }
        }
        index = (int) (currValue % readDataSourceSize);
    } else {
        //随机方式
        index = ThreadLocalRandom.current().nextInt(0, readDataSourceSize);
    }
    return dynamicDataSourceGlobal.name() + index;

    }

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

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

    public void setReadDataSourcePollPattern(int readDataSourcePollPattern) {
    this.readDataSourcePollPattern = readDataSourcePollPattern;
    }
    }
    import org.apache.log4j.Logger;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.reflect.MethodSignature;

import java.lang.reflect.Method;

/**

  • Created by IDEA
  • User: mashaohua
  • Date: 2016-07-07 13:39
  • Desc: 定义选择数据源切面
    */
    public class DynamicDataSourceAspect {

    private static final Logger logger = Logger.getLogger(DynamicDataSourceAspect.class);

    public void pointCut(){};

    public void before(JoinPoint point)
    {
    Object target = point.getTarget();
    String methodName = point.getSignature().getName();
    Class<?>[] clazz = target.getClass().getInterfaces();
    Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
    try {
    Method method = clazz[0].getMethod(methodName, parameterTypes);
    if (method != null && method.isAnnotationPresent(DataSource.class)) {
    DataSource data = method.getAnnotation(DataSource.class);
    DynamicDataSourceHolder.putDataSource(data.value());
    }
    } catch (Exception e) {
    logger.error(String.format("Choose DataSource error, method:%s, msg:%s", methodName, e.getMessage()));
    }
    }

    public void after(JoinPoint point) {
    DynamicDataSourceHolder.clearDataSource();
    }
    }
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="
    http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx-4.1.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop-4.1.xsd"&gt;

    <bean id="abstractDataSource" abstract="true" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
    <!-- 配置获取连接等待超时的时间 -->
    <property name="maxWait" value="60000"/>
    <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
    <property name="timeBetweenEvictionRunsMillis" value="60000"/>
    <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
    <property name="minEvictableIdleTimeMillis" value="300000"/>
    <property name="validationQuery" value="SELECT ‘x‘"/>
    <property name="testWhileIdle" value="true"/>
    <property name="testOnBorrow" value="false"/>
    <property name="testOnReturn" value="false"/>
    <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
    <property name="poolPreparedStatements" value="true"/>
    <property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>
    <property name="filters" value="config"/>
    <property name="connectionProperties" value="config.decrypt=true" />
    </bean>

    <bean id="dataSourceRead1" parent="abstractDataSource">
    <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
    <!-- 基本属性 url、user、password -->
    <property name="url" value="${read1.jdbc.url}"/>
    <property name="username" value="${read1.jdbc.user}"/>
    <property name="password" value="${read1.jdbc.password}"/>
    <!-- 配置初始化大小、最小、最大 -->
    <property name="initialSize" value="${read1.jdbc.initPoolSize}"/>
    <property name="minIdle" value="${read1.jdbc.minPoolSize}"/>
    <property name="maxActive" value="${read1.jdbc.maxPoolSize}"/>
    </bean>

    <bean id="dataSourceRead2" parent="abstractDataSource">
    <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
    <!-- 基本属性 url、user、password -->
    <property name="url" value="${read2.jdbc.url}"/>
    <property name="username" value="${read2.jdbc.user}"/>
    <property name="password" value="${read2.jdbc.password}"/>
    <!-- 配置初始化大小、最小、最大 -->
    <property name="initialSize" value="${read2.jdbc.initPoolSize}"/>
    <property name="minIdle" value="${read2.jdbc.minPoolSize}"/>
    <property name="maxActive" value="${read2.jdbc.maxPoolSize}"/>
    </bean>

    <bean id="dataSourceWrite" parent="abstractDataSource">
    <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
    <!-- 基本属性 url、user、password -->
    <property name="url" value="${write.jdbc.url}"/>
    <property name="username" value="${write.jdbc.user}"/>
    <property name="password" value="${write.jdbc.password}"/>
    <!-- 配置初始化大小、最小、最大 -->
    <property name="initialSize" value="${write.jdbc.initPoolSize}"/>
    <property name="minIdle" value="${write.jdbc.minPoolSize}"/>
    <property name="maxActive" value="${write.jdbc.maxPoolSize}"/>
    </bean>

    <bean id="dataSource" class="com.test.api.dao.datasource.DynamicDataSource">
    <property name="writeDataSource" ref="dataSourceWrite" />
    <property name="readDataSources">
    <list>
    <ref bean="dataSourceRead1" />
    <ref bean="dataSourceRead2" />
    </list>
    </property>
    <!--轮询方式-->
    <property name="readDataSourcePollPattern" value="1" />
    <property name="defaultTargetDataSource" ref="dataSourceWrite"/>
    </bean>

    <tx:annotation-driven transaction-manager="transactionManager"/>

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 针对myBatis的配置项 -->
    <!-- 配置sqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <!-- 实例化sqlSessionFactory时需要使用上述配置好的数据源以及SQL映射文件 -->
    <property name="dataSource" ref="dataSource"/>
    <property name="mapperLocations" value="classpath:mapper/*.xml"/>
    </bean>

    <!-- 配置扫描器 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <!-- 扫描包以及它的子包下的所有映射接口类 -->
    <property name="basePackage" value="com.test.api.dao.inte"/>
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>

    <!-- 配置数据库注解aop -->
    <bean id="dynamicDataSourceAspect" class="com.test.api.dao.datasource.DynamicDataSourceAspect" />
    <aop:config>
    <aop:aspect id="c" ref="dynamicDataSourceAspect">
    <aop:pointcut id="tx" expression="execution( com.test.api.dao.inte...*(..))"/>
    <aop:before pointcut-ref="tx" method="before"/>
    <aop:after pointcut-ref="tx" method="after"/>
    </aop:aspect>
    </aop:config>
    <!-- 配置数据库注解aop -->
    </beans>

方案3
通过Mybatis的Plugin在业务层实现数据库读写分离,在MyBatis创建Statement对象前通过拦截器选择真正的数据源,在拦截器中根据方法名称不同(select、update、insert、delete)选择数据源。

优点:原有代码不变,支持多读,易扩展
缺点:
实现方式

/**

  • Created by IDEA
  • User: mashaohua
  • Date: 2016-07-19 15:40
  • Desc: 创建Connection代理接口
    */
    public interface ConnectionProxy extends Connection {

    /**

    • 根据传入的读写分离需要的key路由到正确的connection
    • @param key 数据源标识
    • @return
      */
      Connection getTargetConnection(String key);
      }
      import java.lang.reflect.InvocationHandler;
      import java.lang.reflect.InvocationTargetException;
      import java.lang.reflect.Method;
      import java.lang.reflect.Proxy;
      import java.sql.Connection;
      import java.sql.SQLException;
      import java.util.ArrayList;
      import java.util.List;
      import java.util.logging.Logger;

import javax.sql.DataSource;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.jdbc.datasource.lookup.DataSourceLookup;
import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;
import org.springframework.util.Assert;

public abstract class AbstractDynamicDataSourceProxy extends AbstractDataSource implements InitializingBean {

private List&lt;Object&gt; readDataSources;
private List&lt;DataSource&gt; resolvedReadDataSources;

private Object writeDataSource;
private DataSource resolvedWriteDataSource;

private int readDataSourcePollPattern = 0;

private int readDsSize;

private boolean defaultAutoCommit = true;
private int defaultTransactionIsolation = Connection.TRANSACTION_READ_COMMITTED;

public static final String READ = "read";

public static final String WRITE = "write";

private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();

@Override
public Connection getConnection() throws SQLException {
    return (Connection) Proxy.newProxyInstance(
            com.autohome.api.dealer.tuan.dao.rwmybatis.ConnectionProxy.class.getClassLoader(),
            new Class[] {com.autohome.api.dealer.tuan.dao.rwmybatis.ConnectionProxy.class},
            new RWConnectionInvocationHandler());
}

@Override
public Connection getConnection(String username, String password)
        throws SQLException {
    return (Connection) Proxy.newProxyInstance(
            com.autohome.api.dealer.tuan.dao.rwmybatis.ConnectionProxy.class.getClassLoader(),
            new Class[] {com.autohome.api.dealer.tuan.dao.rwmybatis.ConnectionProxy.class},
            new RWConnectionInvocationHandler(username,password));
}

public int getReadDsSize(){
    return readDsSize;
}

public List&lt;DataSource&gt; getResolvedReadDataSources() {
    return resolvedReadDataSources;
}

public void afterPropertiesSet() throws Exception {

    if(writeDataSource == null){
        throw new IllegalArgumentException("Property ‘writeDataSource‘ is required");
    }
    this.resolvedWriteDataSource = resolveSpecifiedDataSource(writeDataSource);

    resolvedReadDataSources = new ArrayList&lt;DataSource&gt;(readDataSources.size());
    for(Object item : readDataSources){
        resolvedReadDataSources.add(resolveSpecifiedDataSource(item));
    }
    readDsSize = readDataSources.size();
}

protected DataSource determineTargetDataSource(String key) {
    Assert.notNull(this.resolvedReadDataSources, "DataSource router not initialized");
    if(WRITE.equals(key)){
        return resolvedWriteDataSource;
    }else{
        return loadReadDataSource();
    }
}

public Logger getParentLogger() {
    // NOOP Just ignore
    return null;
}

/**
 * 获取真实的data source
 * @param dataSource (jndi | real data source)
 * @return
 * @throws IllegalArgumentException
 */
protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
    if (dataSource instanceof DataSource) {
        return (DataSource) dataSource;
    }
    else if (dataSource instanceof String) {
        return this.dataSourceLookup.getDataSource((String) dataSource);
    }
    else {
        throw new IllegalArgumentException(
                "Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
    }
}

protected abstract DataSource loadReadDataSource();

public void setReadDsSize(int readDsSize) {
    this.readDsSize = readDsSize;
}

public List&lt;Object&gt; getReadDataSources() {
    return readDataSources;
}

public void setReadDataSources(List&lt;Object&gt; readDataSources) {
    this.readDataSources = readDataSources;
}

public Object getWriteDataSource() {
    return writeDataSource;
}

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

public void setResolvedReadDataSources(List&lt;DataSource&gt; resolvedReadDataSources) {
    this.resolvedReadDataSources = resolvedReadDataSources;
}

public DataSource getResolvedWriteDataSource() {
    return resolvedWriteDataSource;
}

public void setResolvedWriteDataSource(DataSource resolvedWriteDataSource) {
    this.resolvedWriteDataSource = resolvedWriteDataSource;
}

public int getReadDataSourcePollPattern() {
    return readDataSourcePollPattern;
}

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

/**
 * Invocation handler that defers fetching an actual JDBC Connection
 * until first creation of a Statement.
 */
private class RWConnectionInvocationHandler implements InvocationHandler {

    private String username;

    private String password;

    private Boolean readOnly = Boolean.FALSE;

    private Integer transactionIsolation;

    private Boolean autoCommit;

    private boolean closed = false;

    private Connection target;

    public RWConnectionInvocationHandler() {

    }

    public RWConnectionInvocationHandler(String username, String password) {
        this();
        this.username = username;
        this.password = password;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // Invocation on ConnectionProxy interface coming in...

        if (method.getName().equals("equals")) {
            // We must avoid fetching a target Connection for "equals".
            // Only consider equal when proxies are identical.
            return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE);
        }
        else if (method.getName().equals("hashCode")) {
            // We must avoid fetching a target Connection for "hashCode",
            // and we must return the same hash code even when the target
            // Connection has been fetched: use hashCode of Connection proxy.
            return new Integer(System.identityHashCode(proxy));
        }
        else if (method.getName().equals("getTargetConnection")) {
            // Handle getTargetConnection method: return underlying connection.
            return getTargetConnection(method,args);
        }

        if (!hasTargetConnection()) {
            // No physical target Connection kept yet -&gt;
            // resolve transaction demarcation methods without fetching
            // a physical JDBC Connection until absolutely necessary.

            if (method.getName().equals("toString")) {
                return "RW Routing DataSource Proxy";
            }
            else if (method.getName().equals("isReadOnly")) {
                return this.readOnly;
            }
            else if (method.getName().equals("setReadOnly")) {
                this.readOnly = (Boolean) args[0];
                return null;
            }
            else if (method.getName().equals("getTransactionIsolation")) {
                if (this.transactionIsolation != null) {
                    return this.transactionIsolation;
                }
                return defaultTransactionIsolation;
                // Else fetch actual Connection and check there,
                // because we didn‘t have a default specified.
            }
            else if (method.getName().equals("setTransactionIsolation")) {
                this.transactionIsolation = (Integer) args[0];
                return null;
            }
            else if (method.getName().equals("getAutoCommit")) {
                if (this.autoCommit != null)
                    return this.autoCommit;
                return defaultAutoCommit;
                // Else fetch actual Connection and check there,
                // because we didn‘t have a default specified.
            }
            else if (method.getName().equals("setAutoCommit")) {
                this.autoCommit = (Boolean) args[0];
                return null;
            }
            else if (method.getName().equals("commit")) {
                // Ignore: no statements created yet.
                return null;
            }
            else if (method.getName().equals("rollback")) {
                // Ignore: no statements created yet.
                return null;
            }
            else if (method.getName().equals("getWarnings")) {
                return null;
            }
            else if (method.getName().equals("clearWarnings")) {
                return null;
            }
            else if (method.getName().equals("isClosed")) {
                return (this.closed ? Boolean.TRUE : Boolean.FALSE);
            }
            else if (method.getName().equals("close")) {
                // Ignore: no target connection yet.
                this.closed = true;
                return null;
            }
            else if (this.closed) {
                // Connection proxy closed, without ever having fetched a
                // physical JDBC Connection: throw corresponding SQLException.
                throw new SQLException("Illegal operation: connection is closed");
            }
        }

        // Target Connection already fetched,
        // or target Connection necessary for current operation -&gt;
        // invoke method on target connection.
        try {
            return method.invoke(target, args);
        }
        catch (InvocationTargetException ex) {
            throw ex.getTargetException();
        }
    }

    /**
     * Return whether the proxy currently holds a target Connection.
     */
    private boolean hasTargetConnection() {
        return (this.target != null);
    }

    /**
     * Return the target Connection, fetching it and initializing it if necessary.
     */
    private Connection getTargetConnection(Method operation,Object[] args) throws SQLException {

        if (this.target == null) {
            String key = (String) args[0];
            // No target Connection held -&gt; fetch one.
            if (logger.isDebugEnabled()) {
                logger.debug("Connecting to database for operation ‘" + operation.getName() + "‘");
            }

            // Fetch physical Connection from DataSource.
            this.target = (this.username != null) ?
                    determineTargetDataSource(key).getConnection(this.username, this.password) :
                    determineTargetDataSource(key).getConnection();

            // If we still lack default connection properties, check them now.
            //checkDefaultConnectionProperties(this.target);

            // Apply kept transaction settings, if any.
            if (this.readOnly.booleanValue()) {
                this.target.setReadOnly(this.readOnly.booleanValue());
            }
            if (this.transactionIsolation != null) {
                this.target.setTransactionIsolation(this.transactionIsolation.intValue());
            }
            if (this.autoCommit != null && this.autoCommit.booleanValue() != this.target.getAutoCommit()) {
                this.target.setAutoCommit(this.autoCommit.booleanValue());
            }
        }

        else {
            // Target Connection already held -&gt; return it.
            if (logger.isDebugEnabled()) {
                logger.debug("Using existing database connection for operation ‘" + operation.getName() + "‘");
            }
        }

        return this.target;
    }
}

}
import javax.sql.DataSource;
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: mashaohua
  • Date: 2016-07-19 16:04
  • Desc:
    */
    public class DynamicRoutingDataSourceProxy extends AbstractDynamicDataSourceProxy {

    private AtomicLong counter = new AtomicLong(0);

    private static final Long MAX_POOL = Long.MAX_VALUE;

    private final Lock lock = new ReentrantLock();

    @Override
    protected DataSource loadReadDataSource() {
    int index = 1;

    if(getReadDataSourcePollPattern() == 1) {
        //轮询方式
        long currValue = counter.incrementAndGet();
        if((currValue + 1) &gt;= MAX_POOL) {
            try {
                lock.lock();
                if((currValue + 1) &gt;= MAX_POOL) {
                    counter.set(0);
                }
            } finally {
                lock.unlock();
            }
        }
        index = (int) (currValue % getReadDsSize());
    } else {
        //随机方式
        index = ThreadLocalRandom.current().nextInt(0, getReadDsSize());
    }
    return getResolvedReadDataSources().get(index);

    }
    }
    import org.apache.ibatis.executor.statement.RoutingStatementHandler;
    import org.apache.ibatis.executor.statement.StatementHandler;
    import org.apache.ibatis.mapping.MappedStatement;
    import org.apache.ibatis.mapping.SqlCommandType;
    import org.apache.ibatis.plugin.*;

import java.sql.Connection;
import java.util.Properties;

/**

  • 拦截器
    */
    @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) })
    public class DynamicPlugin implements Interceptor {

    public Object intercept(Invocation invocation) throws Throwable {

    Connection conn = (Connection)invocation.getArgs()[0];
    //如果是采用了我们代理,则路由数据源
    if(conn instanceof com.autohome.api.dealer.tuan.dao.rwmybatis.ConnectionProxy){
        StatementHandler statementHandler = (StatementHandler) invocation
                .getTarget();
    
        MappedStatement mappedStatement = null;
        if (statementHandler instanceof RoutingStatementHandler) {
            StatementHandler delegate = (StatementHandler) ReflectionUtils
                    .getFieldValue(statementHandler, "delegate");
            mappedStatement = (MappedStatement) ReflectionUtils.getFieldValue(
                    delegate, "mappedStatement");
        } else {
            mappedStatement = (MappedStatement) ReflectionUtils.getFieldValue(
                    statementHandler, "mappedStatement");
        }
        String key = AbstractDynamicDataSourceProxy.WRITE;
    
        if(mappedStatement.getSqlCommandType() == SqlCommandType.SELECT){
            key = AbstractDynamicDataSourceProxy.READ;
        }else{
            key = AbstractDynamicDataSourceProxy.WRITE;
        }
    
        ConnectionProxy connectionProxy = (ConnectionProxy)conn;
        connectionProxy.getTargetConnection(key);
    
    }
    
    return invocation.proceed();

    }

    public Object plugin(Object target) {

    return Plugin.wrap(target, this);

    }

    public void setProperties(Properties properties) {
    //NOOP

    }

}
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;

import java.lang.reflect.*;

public class ReflectionUtils {

private static final Log logger = LogFactory.getLog(ReflectionUtils.class);

/**
 * 直接设置对象属性值,无视private/protected修饰符,不经过setter函数.
 */
public static void setFieldValue(final Object object, final String fieldName, final Object value) {
    Field field = getDeclaredField(object, fieldName);

    if (field == null)
        throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + object + "]");

    makeAccessible(field);

    try {
        field.set(object, value);
    } catch (IllegalAccessException e) {

    }
}

/**
 * 直接读取对象属性值,无视private/protected修饰符,不经过getter函数.
 */
public static Object getFieldValue(final Object object, final String fieldName) {
    Field field = getDeclaredField(object, fieldName);

    if (field == null)
        throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + object + "]");

    makeAccessible(field);

    Object result = null;
    try {
        result = field.get(object);
    } catch (IllegalAccessException e) {

    }
    return result;
}

/**
 * 直接调用对象方法,无视private/protected修饰符.
 */
public static Object invokeMethod(final Object object, final String methodName, final Class&lt;?&gt;[] parameterTypes,
        final Object[] parameters) throws InvocationTargetException {
    Method method = getDeclaredMethod(object, methodName, parameterTypes);
    if (method == null)
        throw new IllegalArgumentException("Could not find method [" + methodName + "] on target [" + object + "]");

    method.setAccessible(true);

    try {
        return method.invoke(object, parameters);
    } catch (IllegalAccessException e) {

    }

    return null;
}

/**
 * 循环向上转型,获取对象的DeclaredField.
 */
protected static Field getDeclaredField(final Object object, final String fieldName) {
    for (Class&lt;?&gt; superClass = object.getClass(); superClass != Object.class; superClass = superClass
            .getSuperclass()) {
        try {
            return superClass.getDeclaredField(fieldName);
        } catch (NoSuchFieldException e) {
        }
    }
    return null;
}

/**
 * 循环向上转型,获取对象的DeclaredField.
 */
protected static void makeAccessible(final Field field) {
    if (!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())) {
        field.setAccessible(true);
    }
}

/**
 * 循环向上转型,获取对象的DeclaredMethod.
 */
protected static Method getDeclaredMethod(Object object, String methodName, Class&lt;?&gt;[] parameterTypes) {
    for (Class&lt;?&gt; superClass = object.getClass(); superClass != Object.class; superClass = superClass
            .getSuperclass()) {
        try {
            return superClass.getDeclaredMethod(methodName, parameterTypes);
        } catch (NoSuchMethodException e) {

        }
    }
    return null;
}

/**
 * 通过反射,获得Class定义中声明的父类的泛型参数的类型.
 * eg.
 * public UserDao extends HibernateDao&lt;User&gt;
 *
 * @param clazz The class to introspect
 * @return the first generic declaration, or Object.class if cannot be determined
 */
@SuppressWarnings("unchecked")
public static &lt;T&gt; Class&lt;T&gt; getSuperClassGenricType(final Class clazz) {
    return getSuperClassGenricType(clazz, 0);
}

/**
 * 通过反射,获得Class定义中声明的父类的泛型参数的类型.
 * eg.
 * public UserDao extends HibernateDao&lt;User&gt;
 *
 * @param clazz The class to introspect
 * @return the first generic declaration, or Object.class if cannot be determined
 */
@SuppressWarnings("unchecked")
public static Class getSuperClassGenricType(final Class clazz, final int index) {

    Type genType = clazz.getGenericSuperclass();

    if (!(genType instanceof ParameterizedType)) {
        logger.warn(clazz.getSimpleName() + "‘s superclass not ParameterizedType");
        return Object.class;
    }

    Type[] params = ((ParameterizedType) genType).getActualTypeArguments();

    if (index &gt;= params.length || index &lt; 0) {
        logger.warn("Index: " + index + ", Size of " + clazz.getSimpleName() + "‘s Parameterized Type: "
                + params.length);
        return Object.class;
    }
    if (!(params[index] instanceof Class)) {
        logger.warn(clazz.getSimpleName() + " not set the actual class on superclass generic parameter");
        return Object.class;
    }

    return (Class) params[index];
}

/**
 * 将反射时的checked exception转换为unchecked exception.
 */
public static IllegalArgumentException convertToUncheckedException(Exception e) {
    if (e instanceof IllegalAccessException || e instanceof IllegalArgumentException
            || e instanceof NoSuchMethodException)
        return new IllegalArgumentException("Refelction Exception.", e);
    else
        return new IllegalArgumentException(e);
}

}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD SQL Map Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd"&gt;
<configuration>
<plugins>
<plugin interceptor="com.test.api.dao.mybatis.DynamicPlugin">
</plugin>
</plugins>

</configuration>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.1.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.1.xsd"&gt;

&lt;bean id="abstractDataSource" abstract="true" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"&gt;
    &lt;property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/&gt;
    &lt;!-- 配置获取连接等待超时的时间 --&gt;
    &lt;property name="maxWait" value="60000"/&gt;
    &lt;!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 --&gt;
    &lt;property name="timeBetweenEvictionRunsMillis" value="60000"/&gt;
    &lt;!-- 配置一个连接在池中最小生存的时间,单位是毫秒 --&gt;
    &lt;property name="minEvictableIdleTimeMillis" value="300000"/&gt;
    &lt;property name="validationQuery" value="SELECT ‘x‘"/&gt;
    &lt;property name="testWhileIdle" value="true"/&gt;
    &lt;property name="testOnBorrow" value="false"/&gt;
    &lt;property name="testOnReturn" value="false"/&gt;
    &lt;!-- 打开PSCache,并且指定每个连接上PSCache的大小 --&gt;
    &lt;property name="poolPreparedStatements" value="true"/&gt;
    &lt;property name="maxPoolPreparedStatementPerConnectionSize" value="20"/&gt;
    &lt;property name="filters" value="config"/&gt;
    &lt;property name="connectionProperties" value="config.decrypt=true" /&gt;
&lt;/bean&gt;

&lt;bean id="dataSourceRead1" parent="abstractDataSource"&gt;
    &lt;property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/&gt;
    &lt;!-- 基本属性 url、user、password --&gt;
    &lt;property name="url" value="${read1.jdbc.url}"/&gt;
    &lt;property name="username" value="${read1.jdbc.user}"/&gt;
    &lt;property name="password" value="${read1.jdbc.password}"/&gt;
    &lt;!-- 配置初始化大小、最小、最大 --&gt;
    &lt;property name="initialSize" value="${read1.jdbc.initPoolSize}"/&gt;
    &lt;property name="minIdle" value="${read1.jdbc.minPoolSize}"/&gt;
    &lt;property name="maxActive" value="${read1.jdbc.maxPoolSize}"/&gt;
&lt;/bean&gt;

&lt;bean id="dataSourceRead2" parent="abstractDataSource"&gt;
    &lt;property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/&gt;
    &lt;!-- 基本属性 url、user、password --&gt;
    &lt;property name="url" value="${read2.jdbc.url}"/&gt;
    &lt;property name="username" value="${read2.jdbc.user}"/&gt;
    &lt;property name="password" value="${read2.jdbc.password}"/&gt;
    &lt;!-- 配置初始化大小、最小、最大 --&gt;
    &lt;property name="initialSize" value="${read2.jdbc.initPoolSize}"/&gt;
    &lt;property name="minIdle" value="${read2.jdbc.minPoolSize}"/&gt;
    &lt;property name="maxActive" value="${read2.jdbc.maxPoolSize}"/&gt;
&lt;/bean&gt;

&lt;bean id="dataSourceWrite" parent="abstractDataSource"&gt;
    &lt;property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/&gt;
    &lt;!-- 基本属性 url、user、password --&gt;
    &lt;property name="url" value="${write.jdbc.url}"/&gt;
    &lt;property name="username" value="${write.jdbc.user}"/&gt;
    &lt;property name="password" value="${write.jdbc.password}"/&gt;
    &lt;!-- 配置初始化大小、最小、最大 --&gt;
    &lt;property name="initialSize" value="${write.jdbc.initPoolSize}"/&gt;
    &lt;property name="minIdle" value="${write.jdbc.minPoolSize}"/&gt;
    &lt;property name="maxActive" value="${write.jdbc.maxPoolSize}"/&gt;
&lt;/bean&gt;

&lt;bean id="dataSource" class="com.test.api.dao.datasource.DynamicRoutingDataSourceProxy"&gt;
    &lt;property name="writeDataSource" ref="dataSourceWrite" /&gt;
    &lt;property name="readDataSources"&gt;
        &lt;list&gt;
            &lt;ref bean="dataSourceRead1" /&gt;
            &lt;ref bean="dataSourceRead2" /&gt;
        &lt;/list&gt;
    &lt;/property&gt;
    &lt;!--轮询方式--&gt;
    &lt;property name="readDataSourcePollPattern" value="1" /&gt;
&lt;/bean&gt;

&lt;tx:annotation-driven transaction-manager="transactionManager"/&gt;

&lt;bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"&gt;
    &lt;property name="dataSource" ref="dataSource"/&gt;
&lt;/bean&gt;

&lt;!-- 针对myBatis的配置项 --&gt;
&lt;!-- 配置sqlSessionFactory --&gt;
&lt;bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"&gt;
    &lt;!-- 实例化sqlSessionFactory时需要使用上述配置好的数据源以及SQL映射文件 --&gt;
    &lt;property name="dataSource" ref="dataSource"/&gt;
    &lt;property name="mapperLocations" value="classpath:mapper/*.xml"/&gt;
    &lt;property name="configLocation" value="classpath:mybatis-plugin-config.xml" /&gt;
&lt;/bean&gt;

&lt;bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"&gt;
    &lt;constructor-arg ref="sqlSessionFactory" /&gt;
&lt;/bean&gt;
&lt;!-- 通过扫描的模式,扫描目录下所有的mapper, 根据对应的mapper.xml为其生成代理类--&gt;
&lt;bean id="mapper" class="org.mybatis.spring.mapper.MapperScannerConfigurer"&gt;
    &lt;property name="basePackage" value="com.test.api.dao.inte" /&gt;
    &lt;property name="sqlSessionTemplate" ref="sqlSessionTemplate"&gt;&lt;/property&gt;
&lt;/bean&gt;

</beans>

方案4
如果你的后台结构是spring+mybatis,可以通过spring的AbstractRoutingDataSource和mybatis Plugin拦截器实现非常友好的读写分离,原有代码不需要任何改变。推荐第四种方案

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import java.util.HashMap;
import java.util.Map;

/**

  • Created by IDEA
  • User: mashaohua
  • Date: 2016-07-14 10:56
  • Desc: 动态数据源实现读写分离
    */
    public class DynamicDataSource extends AbstractRoutingDataSource {

    private Object writeDataSource; //写数据源

    private Object readDataSource; //读数据源

    @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(readDataSource != null) {
    targetDataSources.put(DynamicDataSourceGlobal.READ.name(), readDataSource);
    }
    setTargetDataSources(targetDataSources);
    super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {

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

    }

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

    public Object getWriteDataSource() {
    return writeDataSource;
    }

    public Object getReadDataSource() {
    return readDataSource;
    }

    public void setReadDataSource(Object readDataSource) {
    this.readDataSource = readDataSource;
    }
    }
    /**

  • Created by IDEA
  • User: mashaohua
  • Date: 2016-07-14 10:58
  • 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;

/**

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

/**

Spring+MyBatis实现数据库读写分离方案

原文地址:http://blog.51cto.com/13917525/2306569

时间: 2024-10-26 12:01:47

Spring+MyBatis实现数据库读写分离方案的相关文章

springboot+mybatis实现数据库读写分离

本文不包含数据库主从配置. 实现思路:在项目中配置多数据源,通过代码控制访问哪一个数据源. spring-jdbc为我们提供了AbstractRoutingDataSource,DataSource的抽象实现,基于查找键,返回不通不同的数据源.编写我们自己的动态数据源类DynamicDataSource继承AbstractRoutingDataSource,实现determineCurrentLookupKey方法. 配置一个spring config类DataSourceConfig,把Dyn

spring+mybatis利用interceptor(plugin)兑现数据库读写分离

使用spring的动态路由实现数据库负载均衡 系统中存在的多台服务器是“地位相当”的,不过,同一时间他们都处于活动(Active)状态,处于负载均衡等因素考虑,数据访问请求需要在这几台数据库服务器之间进行合理分配, 这个时候,通过统一的一个DataSource来屏蔽这种请求分配的需求,从而屏蔽数据访问类与具体DataSource的耦合: 系统中存在的多台数据库服务器现在地位可能相当也可能不相当,但数据访问类在系统启动时间无法明确到底应该使用哪一个数据源进行数据访问,而必须在系统运行期间通过某种条

Spring + Mybatis项目实现数据库读写分离

主要思路:通过实现AbstractRoutingDataSource类来动态管理数据源,利用面向切面思维,每一次进入service方法前,选择数据源. 1.首先pom.xml中添加aspect依赖 <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.8.9</version> </depen

Spring+Mybatis实现主从数据库读写分离

Spring+Mybatis实现主从数据库读写分离 采用配置+注解的方式. 自定义@DataSource注解 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME

161920、使用Spring AOP实现MySQL数据库读写分离案例分析

一.前言 分布式环境下数据库的读写分离策略是解决数据库读写性能瓶颈的一个关键解决方案,更是最大限度了提高了应用中读取 (Read)数据的速度和并发量. 在进行数据库读写分离的时候,我们首先要进行数据库的主从配置,最简单的是一台Master和一台Slave(大型网站系统的话,当然会很复杂,这里只是分析了最简单的情况).通过主从配置主从数据库保持了相同的数据,我们在进行读操作的时候访问从数据库Slave,在进行写操作的时候访问主数据库Master.这样的话就减轻了一台服务器的压力. 在进行读写分离案

使用Spring AOP实现MySQL数据库读写分离案例分析

一.前言 分布式环境下数据库的读写分离策略是解决数据库读写性能瓶颈的一个关键解决方案,更是最大限度了提高了应用中读取 (Read)数据的速度和并发量. 在进行数据库读写分离的时候,我们首先要进行数据库的主从配置,最简单的是一台Master和一台Slave(大型网站系统的话,当然会很复杂,这里只是分析了最简单的情况).通过主从配置主从数据库保持了相同的数据,我们在进行读操作的时候访问从数据库Slave,在进行写操作的时候访问主数据库Master.这样的话就减轻了一台服务器的压力. 在进行读写分离案

Mybatis多数据源读写分离(注解实现)

#### Mybatis多数据源读写分离(注解实现) ------ 首先需要建立两个库进行测试,我这里使用的是master_test和slave_test两个库,两张库都有一张同样的表(偷懒,喜喜),表结构 表名 t_user | 字段名 | 类型 | 备注 | | :------: | :------: | :------: | | id | int | 主键自增ID | | name | varchar | 名称 | ![file](https://img2018.cnblogs.com/b

sql server几种读写分离方案的比较

在生产环境中我们经常会遇到这种情况: 前端的oltp业务很繁忙,但是需要对这些运营数据进行olap,为了不影响前端正常业务,所以需要将数据库进行读写分离. 这里我将几种可以用来进行读写分离的方案总结一下,方案本身并无优劣可言,只看是否适合业务使用场景,所以只把几个方案的特点罗列出来,遇到具体的问题时按自己需求和环境综合考虑后再进行取舍 读写分离方案 实时同步 副本数据是否直接可读 副本数 最小粒度 副本建立索引 环境 缺点 镜像 是 否(需要开启快照,只读) 1 库 否 域/非域(使用证书) 在

Spring+MyBatis双数据库配置

Spring+MyBatis双数据库配置 近期项目中遇到要调用其它数据库的情况.本来仅仅使用一个MySQL数据库.但随着项目内容越来越多,逻辑越来越复杂. 原来一个数据库已经不够用了,须要分库分表.所以决定扩充数据库,正好Spring能够灵活的扩充数据库.以下简单写一篇博文,记录下多数据库配置的过程. 1.项目结构例如以下图: 当中mkhl和ulab分别相应两个数据库模块.同一时候也相应两个不同的功能模块. 2.整个Maven项目的配置文件:pom.xml <project xmlns="