spring+mybatis多数据源配置、读写分离

331 阅读3分钟

参考文章:blog.csdn.net/guying4875/…

监于原文章格式比较乱,并且文章不太好找,所以参考文章重新排版,方便自己加深印象。

说明:文中代码做了部分删减,关键信息做了擦除

原理讲解

实现思路:在spring中配置多个数据源,然后在service层通过注解方式标明方法所要使用的数据源,利用springAOP在service方法执行前根据方法上的注解明确所要使用的数据源。如下图

以上分析可得出,需要抽象出一个DynamicDataSource数据源,在spring中关于数据源的配置都是基于DynamicDataSource,在运行时根据service上的注解从DynamicDataSource选取需要的数据源进行实际的持久化操作;首先,介绍spring 的AbstractRoutingDataSource 类

AbstractRoutingDataSource这个类 是spring2.0以后增加的

我们先来看下AbstractRoutingDataSource的定义(下面开始上代码,只展示了关键的一些代码):

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;
   
   ......
   
   @Override
   public Connection getConnection() throws SQLException {
   	return determineTargetDataSource().getConnection();
   }

   @Override
   public Connection getConnection(String username, String password) throws SQLException {
   	return determineTargetDataSource().getConnection(username, password);
   }
   
   ......

   protected DataSource determineTargetDataSource() {
   	Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
      // 一个后门,留给子类用来重写。获取xml文件中配置的key
   	Object lookupKey = determineCurrentLookupKey();
       // resolvedDataSources 就是解析的xml配置文件,解析的键值对存入一个map中,
   	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;*
   }

   protected abstract Object determineCurrentLookupKey();
}

AbstractRoutingDataSource继承了AbstractDataSource ,而AbstractDataSource 又是DataSource 的子类。

DataSource 是javax.sql 的数据源接口,定义如下:

package javax.sql;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Wrapper;

public interface DataSource  extends CommonDataSource, Wrapper {

 Connection getConnection() throws SQLException;

 Connection getConnection(String username, String password)
   throws SQLException;
}

DataSource 接口定义了2个方法,都是获取数据库连接。我们在看下AbstractRoutingDataSource 如何实现了DataSource接口, AbstractRoutingDataSource.java 我们主要关心两句话

   Object lookupKey = determineCurrentLookupKey();
   DataSource dataSource = this.resolvedDataSources.get(lookupKey);

determineCurrentLookupKey方法返回lookupKey,resolvedDataSources方法就是根据lookupKey从Map中获得数据源。

resolvedDataSources 和determineCurrentLookupKey定义如下:

   // 键值对
   private Map<Object, DataSource> resolvedDataSources;
   // 抽象方法,留个子类实现
   protected abstract Object determineCurrentLookupKey()

看到以上定义,我们是不是有点思路了,resolvedDataSources是Map类型,我们可以把MasterDataSource和SlaveDataSource存到Map中,如下:

keyvalue
masterMasterDataSource
slaveSlaveDataSource

我们在写一个类DynamicDataSource 继承AbstractRoutingDataSource,实现其 determineCurrentLookupKey() 方法,该方法返回Map的key,master或slave。这样就达到了切换数据库的目的。

介绍实现方式:

1. 创建注解类,用来标记service方法所要使用的数据源key

  import java.lang.annotation.*;

  @Target(ElementType.METHOD)
  @Retention(RetentionPolicy.RUNTIME)
  @Documented
  public @interface DataSourceSeparate {

      DataSourceTypeEnum value() default DataSourceTypeEnum.MASTER;
  }

2. 创建DynamicDataSource类,该类继承自AbstractRoutingDataSource

注: DynamicDataSourceHolder类可以省略,跟DynamicDataSource合并成一个类。

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

   public class DynamicDataSource extends AbstractRoutingDataSource {
      @Override
      protected Object determineCurrentLookupKey() {
          return DynamicDataSourceHolder.getDbType();
      }
   }



   // @Description DynamicDataSourceHolder 此类主要就是为了构建一个 ThreadLocal。对当前线程存储一个全局的对象。 然后通过Aop的切面,进行动态的更改 这个全局的 ThreadLocal对象值。
   public abstract class  {
      private static ThreadLocal<String> contextHolder = new ThreadLocal<String>();

      public static String getDbType() {
          String db = contextHolder.get();
          if (db == null) {
              // 默认为主库
              db = "master";
          }
          return db;
      }

      public static void setDbType(String str) {
          contextHolder.set(str);
      }

      //  清理连接类型
      public static void clearDBType() {
          contextHolder.remove();
      }
  }

方法中实现了determineCurrentLookupKey()方法,该方法会从ThreadLocal对象中获取到一个key来表明所要使用的数据源

3. 实现springAop来根据service中方法上的注解设置ThreadLocal对象

package 
import com.fengjr.datasource.DynamicDataSourceHolder;
import com.fengjr.p2p.asset.common.annotations.DataSourceSeparate;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

 //  @Description 默认使用主库,根据切面选择从库
 @Slf4j
 @Order(1)
 @Aspect
 public class DynamicDataSourceAspect {

     @Pointcut("execution(* com..service..*.*(..))  && @annotation(com.annotations.DataSourceSeparate)")
     private void anyMethod() {}//声明一个切入点

     //在service的所有方法执行前,并且带有注解 DataSourceSeparate
     @Before("anyMethod()")
     public void beforeDynamicSource(JoinPoint joinPoint){
         //获取方法,此处可将signature强转为MethodSignature
         MethodSignature signature = (MethodSignature) joinPoint.getSignature();
         Method method = signature.getMethod();
         DataSourceSeparate annotation = method.getAnnotation(DataSourceSeparate.class);
         String key = annotation.value().getKey();
         System.out.println(key);
         try {
             DynamicDataSourceHolder.setDbType(key);
         } catch (Throwable t) {
             log.info(t.getMessage(), t);
         }

     }

     @After("anyMethod()")
     public void afterDynamicSource(){
         try {
             DynamicDataSourceHolder.clearDBType();
         } catch (Throwable t) {
             log.info(t.getMessage(), t);
         }
     }
 }

4. 开始配置spring数据源 application-jdbc.xml

  <?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:util="http://www.springframework.org/schema/util"
         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/util http://www.springframework.org/schema/util/spring-util.xsd">

      <!-- 基础 数据源 -->
      <bean id="abstractDataSource" abstract="true" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
          <property name="maxPoolSize" value="${jdbc.maxPoolSize}"/>
          <property name="minPoolSize" value="${jdbc.minPoolSize}"/>
          <property name="acquireRetryAttempts" value="${jdbc.acquireRetryAttempts}"/>
          <property name="testConnectionOnCheckout" value="${jdbc.testConnectionOnCheckout}"/>
          <property name="breakAfterAcquireFailure" value="${jdbc.breakAfterAcquireFailure}"/>
          <property name="idleConnectionTestPeriod" value="${jdbc.idleConnectionTestPeriod}"/>
          <property name="maxStatements" value="${jdbc.maxStatements}"/>
          <property name="maxIdleTime" value="${jdbc.maxIdleTime}"/>
          <property name="driverClass" value="${jdbc.driver}"/>
      </bean>

      <bean id="dataSource_master_p2p_asset" parent="abstractDataSource">
          <property name="jdbcUrl" value="${mysql.pro}"/>
          <property name="user" value="${mysql.pro.w}"/>
          <property name="password" value="${mysql.pro.pwd.rw}"/>
      </bean>


      <!-- 从数据库 主要负责读数据 -->
      <bean id="dataSource_slave" parent="abstractDataSource">
          <property name="jdbcUrl" value="${mysql.pro.url.read}"/>
          <property name="user" value="${mysql.pro.user.read}" />
          <property name="password" value="${mysql.pro.pwd.read}" />
      </bean>

      <!-- 配置动态数据源 -->
      <bean id="dataSource" class="com.fengjr.datasource.DynamicDataSource" lazy-init="true">
          <property name="targetDataSources">
              <!-- 此处是关键,返回的对象是DataSource, 这是 java.sql包下的一个类。 -->
              <map key-type="java.lang.String" value-type="javax.sql.DataSource">
                  <entry value-ref="dataSource_master" key="master"></entry>
                  <entry value-ref="dataSource_slave" key="slave"></entry>
              </map>
          </property>
          <!-- 设置默认的目标数据源 -->
          <property name="defaultTargetDataSource" ref="dataSource_master" />
      </bean>
  </beans>

5. 实例使用方法

public interface Userservic {  
    //@DataSource("master")   默认查找主库
    public void add(User user);  
    
    @DataSourceSeparate(value = DataSourceTypeEnum.SLAVE)
    public void selectByCondition(User user);  
}

读写分析方法还有很多,这只是其中一种,首次写文,不足指出还请多多见谅。