讀寫分離(spring事務代理+mybaits攔截器實現)

一、讀寫分離

         採用繼承DataSourceTransactionManager控制事務讀寫分離,以及mybatis攔截器控制方法讀寫分離,經過繼承AbstractRoutingDataSource獲取動態數據源。html

1.一、代碼說明

1.1.一、本地數據源管理

建立本地數據源類型管理類,使用ThreadLocal保存本地數據源類型java

bodsite-common - com.bodsite.common.datasource. DataSourceHandlergit

package com.bodsite.common.datasource;spring

/**sql

 * @Description:本地線程數據源數據庫

 * @author bodapache

 * @date安全

 *session

 */mybatis

public class DataSourceHandler {

   public enum DYNAMIC_DATA_SOURCE{

      MASTER,//主庫(寫)

      SLAVE;//從庫(讀)

   }

  

   private static final ThreadLocal<DYNAMIC_DATA_SOURCE> dataSourceThreadLocal = new ThreadLocal<DYNAMIC_DATA_SOURCE>();

  

   protected static void set(DYNAMIC_DATA_SOURCE dynamic_data_source){

      dataSourceThreadLocal.set(dynamic_data_source);

   }

  

   /**

    * 設置爲主庫

    * @author bod

    */

   protected static void setMaster(){

      dataSourceThreadLocal.set(DYNAMIC_DATA_SOURCE.MASTER);

   }

  

   /**

    * 設置爲讀庫

    * @author bod

    */

   protected static void setSlave(){

      dataSourceThreadLocal.set(DYNAMIC_DATA_SOURCE.SLAVE);

   }

 

   /**

    * 判斷是否爲主庫

    * @author bod

    */

   protected static boolean isMaster(){

      return isThis(DYNAMIC_DATA_SOURCE.MASTER);

   }

  

   /**

    * 判斷是否爲從

    * @author bod

    */

   protected static boolean isSlave(){

      return isThis(DYNAMIC_DATA_SOURCE.SLAVE);

   }

 

   protected static boolean isThis(DYNAMIC_DATA_SOURCE dynamic_data_source){

      if(dataSourceThreadLocal.get()==null){

         return false;

      }

      return dynamic_data_source == dataSourceThreadLocal.get();

   }

  

   protected static void DataSoruceClean(){

      dataSourceThreadLocal.remove();

   }

}

1.1.二、mybatis攔截,設置數據源

建立mybatis攔截器,攔截update、query方法,設置數據源,若是有事務,不作攔截,有事務的狀況,在事務管理器中進行設置。

bodsite-common - com.bodsite.common.datasource. DynamicDataSourceInterceptor

package com.bodsite.common.datasource;

 

import java.util.Properties;

 

import org.apache.ibatis.executor.Executor;

import org.apache.ibatis.executor.keygen.SelectKeyGenerator;

import org.apache.ibatis.mapping.MappedStatement;

import org.apache.ibatis.mapping.SqlCommandType;

import org.apache.ibatis.plugin.Interceptor;

import org.apache.ibatis.plugin.Intercepts;

import org.apache.ibatis.plugin.Invocation;

import org.apache.ibatis.plugin.Plugin;

import org.apache.ibatis.plugin.Signature;

import org.apache.ibatis.session.ResultHandler;

import org.apache.ibatis.session.RowBounds;

import org.springframework.transaction.support.TransactionSynchronizationManager;

 

/**

 * @Description: mybatis plugin 攔截器-設置數據源

 * @author bod

 * @date

 *

 */

@Intercepts({

      @Signature(method = "query", type = Executor.class, args = { MappedStatement.class, Object.class,

            RowBounds.class, ResultHandler.class }), // method:方法名,type:類,args:方法參數

      @Signature(method = "update", type = Executor.class, args = { MappedStatement.class, Object.class }) })

public class DynamicDataSourceInterceptor implements Interceptor {

 

   @Override

   public Object intercept(Invocation invocation) throws Throwable {

      // 是否有事務

      boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();

      if (!synchronizationActive) {

         Object[] args = invocation.getArgs();

         MappedStatement ms = (MappedStatement) args[0];

         if(ms.getSqlCommandType().equals(SqlCommandType.SELECT)){//查詢

              //!selectKey 爲自增id查詢主鍵(SELECT LAST_INSERT_ID() )方法,使用主庫

                if(ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {

                DataSourceHandler.setMaster();

                }else{

                DataSourceHandler.setSlave();

                }

         }else{//其餘

            DataSourceHandler.setMaster();

         }

      }

      Object result = invocation.proceed();

      DataSourceHandler.DataSoruceClean();

      return result;

   }

 

   @Override

   public Object plugin(Object target) {

      if (target instanceof Executor) {

         return Plugin.wrap(target, this);

      } else {

         return target;

      }

   }

 

   @Override

   public void setProperties(Properties properties) {

 

   }

 

}

 

1.1.三、spring事務管理,設置數據源

建立動態事務管理類,繼承DataSourceTransactionManager,根據讀寫判斷,設置數據源。

bodsite-common - com.bodsite.common.datasource. DataSourceTransactionManager

   package com.bodsite.common.datasource;

 

import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import org.springframework.transaction.TransactionDefinition;

 

/**

 *

 * @Description:根據事務這是數據源(必須有事務才能進入)

 * @author bod

 * @date

 *

 */

public class  DynamicDataSourceTransactionManager extends DataSourceTransactionManager{

 

   /**

    *

    */

   private static final long serialVersionUID = 1L;

 

   @Override

   protected void doBegin(Object transaction, TransactionDefinition definition) {

      boolean readOnly = definition.isReadOnly();

      if(readOnly){//只讀

         DataSourceHandler.setSlave();

      }else{//讀寫

         DataSourceHandler.setMaster();

      }

      super.doBegin(transaction, definition);

   }

 

   @Override

   protected void doCleanupAfterCompletion(Object transaction) {

      super.doCleanupAfterCompletion(transaction);

      DataSourceHandler.DataSoruceClean();

   }

  

}

 

1.1.四、動態獲取數據源

建立動態獲取數據源類,繼承AbstractRoutingDataSource,根據讀寫判斷,設置數據源。

提供兩種獲取數據源方式:

一、經過重寫determineCurrentLookupKey方法,返回數據源名稱,走AbstractRoutingDataSource的determineTargetDataSource(根據determineCurrentLookupKey返回的名稱)方法獲取數據源。

二、經過重寫determineTargetDataSource。直接返回數據源。

bodsite-common - com.bodsite.common.datasource. DynamicDataSource

package com.bodsite.common.datasource;

 

import java.util.HashMap;

import java.util.List;

import java.util.Map;

import java.util.concurrent.ThreadLocalRandom;

import java.util.concurrent.atomic.AtomicInteger;

 

import javax.sql.DataSource;

 

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

 

import com.bodsite.common.logger.Logger;

import com.bodsite.common.logger.LoggerFactory;

 

/**

 * @Description:動態數據源

 * @author bod

 * @date

 *

 */

public class DynamicDataSource extends AbstractRoutingDataSource {

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

   private DataSource master;

   private List<DataSource> slaves;

    private int slaveSize; //讀數據源個數

   private Integer strategy;// 默認,0:輪詢,1,隨機

   private AtomicInteger counter = new AtomicInteger();

   private Map<Object, Object> targetDataSources = new HashMap<>();

 

   /******************* 一、加載數據源關係, 設置數據源名,會根據數據源名返回數據源 ************/

   /**

    * 設置數據源名

    */

   @Override

   protected Object determineCurrentLookupKey() {

      return getDataSourceName();

   }

 

   /**

    * 設置數據源映射關係

    */

   @Override

   public void afterPropertiesSet() {

      if (this.master == null) {

         throw new IllegalArgumentException("Property 'targetDataSources' is required");

      }

      setDefaultTargetDataSource(master);

      targetDataSources.put(DataSourceHandler.DYNAMIC_DATA_SOURCE.MASTER.name(), master);

      if (slaves != null && !slaves.isEmpty()) {

         this.slaveSize = slaves.size();

         for (int i = 0; i < slaveSize; i++) {

            targetDataSources.put(DataSourceHandler.DYNAMIC_DATA_SOURCE.SLAVE.name() + i, slaves.get(i));

         }

      }

      setTargetDataSources(targetDataSources);

      super.afterPropertiesSet();

   }

 

   /******************* 二、直接返回數據源 ***********************/

   /**

    * 原方法:根據數據源名稱返回數據源,自定義直接返回數據源

    */

   /*@Override

   protected DataSource determineTargetDataSource() {

      return (DataSource) targetDataSources.get(getDataSourceName());

   }*/

 

   /**

    * 獲取數據源名稱

    * @author bod

    */

   public String getDataSourceName() {

      String dataSourceName = null;

      if (DataSourceHandler.isMaster()) {

         dataSourceName = DataSourceHandler.DYNAMIC_DATA_SOURCE.MASTER.name();

      } else if (DataSourceHandler.isSlave() && slaves != null && !slaves.isEmpty()) {

         int index = 0;

         if (strategy == null || strategy == 0) {

            int count = counter.incrementAndGet();

            index = count%slaveSize;

         }else if(strategy == 1){

               index = ThreadLocalRandom.current().nextInt(0, slaveSize);

         }

         dataSourceName = DataSourceHandler.DYNAMIC_DATA_SOURCE.SLAVE.name()+index;

      }else{

         dataSourceName = DataSourceHandler.DYNAMIC_DATA_SOURCE.MASTER.name();

      }

      logger.info("This data source name is "+dataSourceName);

      return dataSourceName;

   }

   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;

   }

 

   public Integer getStrategy() {

      return strategy;

   }

 

   public void setStrategy(Integer strategy) {

      this.strategy = strategy;

   }

 

}

 

 

1.1.五、application-mybatis.xml配置

bodsite-site-service - application-mybatis.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

   xmlns:context="http://www.springframework.org/schema/context"

   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.3.xsd

      http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd

            http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd

      http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">

   <!-- druid 鏈接池配置信息 http://www.cnblogs.com/SummerinShire/p/5828888.html -->

   <!-- druid 數據庫鏈接池 -->

   <bean id="masterDataSource" class="com.alibaba.druid.pool.DruidDataSource"

      init-method="init" destroy-method="close">

      <!-- 鏈接數據庫信息 -->

      <property name="driverClassName" value="${master.jdbc.driver}" />

      <property name="url" value="${master.jdbc.url}" />

      <property name="username" value="${master.jdbc.username}" />

      <property name="password" value="${master.jdbc.password}" />

 

      <!-- 初始化時創建物理鏈接的個數。初始化發生在顯示調用init方法,或者第一次getConnection時 -->

      <property name="initialSize" value="1" />

      <!-- 最大鏈接池數量 -->

      <property name="maxActive" value="50" />

      <!-- 最小鏈接池數量 -->

      <property name="minIdle" value="10" />

      <!-- 獲取鏈接時最大等待時間,單位毫秒 -->

      <property name="maxWait" value="60000" />

      <!-- 配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接,單位是毫秒 -->

      <property name="timeBetweenEvictionRunsMillis" value="60000" />

      <!-- 配置一個鏈接在池中最小生存的時間,單位是毫秒 -->

      <property name="minEvictableIdleTimeMillis" value="300000" />

      <!-- 用來檢測鏈接是否有效的sq -->

      <property name="validationQuery" value="SELECT 'x' FROM DUAL" />

      <!-- 建議配置爲true,不影響性能,而且保證安全性 -->

      <property name="testWhileIdle" value="true" />

      <!-- 申請鏈接時執行validationQuery檢測鏈接是否有效,作了這個配置會下降性能。 -->

      <property name="testOnBorrow" value="false" />

      <!-- 歸還鏈接時執行validationQuery檢測鏈接是否有效,作了這個配置會下降性能。 -->

      <property name="testOnReturn" value="false" />

      <!-- 屬性類型是字符串,經過別名的方式配置擴展插件,經常使用的插件有:監控統計用的filter:stat 日誌用的filter:log4j 防護sql注入的filter:wall -->

      <property name="filters" value="stat" />

   </bean>

 

   <bean id="slaveDataSource" class="com.alibaba.druid.pool.DruidDataSource"

      init-method="init" destroy-method="close">

      <!-- 鏈接數據庫信息 -->

      <property name="driverClassName" value="${slave1.jdbc.driver}" />

      <property name="url" value="${slave1.jdbc.url}" />

      <property name="username" value="${slave1.jdbc.username}" />

      <property name="password" value="${slave1.jdbc.password}" />

 

      <!-- 初始化時創建物理鏈接的個數。初始化發生在顯示調用init方法,或者第一次getConnection時 -->

      <property name="initialSize" value="1" />

      <!-- 最大鏈接池數量 -->

      <property name="maxActive" value="50" />

      <!-- 最小鏈接池數量 -->

      <property name="minIdle" value="10" />

      <!-- 獲取鏈接時最大等待時間,單位毫秒 -->

      <property name="maxWait" value="60000" />

      <!-- 配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接,單位是毫秒 -->

      <property name="timeBetweenEvictionRunsMillis" value="60000" />

      <!-- 配置一個鏈接在池中最小生存的時間,單位是毫秒 -->

      <property name="minEvictableIdleTimeMillis" value="300000" />

      <!-- 用來檢測鏈接是否有效的sq -->

      <property name="validationQuery" value="SELECT 'x' FROM DUAL" />

      <!-- 建議配置爲true,不影響性能,而且保證安全性 -->

      <property name="testWhileIdle" value="true" />

      <!-- 申請鏈接時執行validationQuery檢測鏈接是否有效,作了這個配置會下降性能。 -->

      <property name="testOnBorrow" value="false" />

      <!-- 歸還鏈接時執行validationQuery檢測鏈接是否有效,作了這個配置會下降性能。 -->

      <property name="testOnReturn" value="false" />

      <!-- 屬性類型是字符串,經過別名的方式配置擴展插件,經常使用的插件有:監控統計用的filter:stat 日誌用的filter:log4j 防護sql注入的filter:wall -->

      <property name="filters" value="stat" />

   </bean>

   <!-- 動態數據源 -->

   <bean id="dataSource" class="com.bodsite.common.datasource.DynamicDataSource">

      <property name="master" ref="masterDataSource" />

      <property name="slaves">

         <list>

            <ref bean="slaveDataSource" />

         </list>

      </property>

      <property name="strategy" value="0"/>

   </bean>

 

 

   <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

      <property name="dataSource" ref="dataSource" />

      <property name="configLocation" value="classpath:mybatis-config.xml" />

      <property name="mapperLocations" value="classpath:/mappings/**/*.xml" />

   </bean>

   <!-- 定義事務 -->

   <bean id="transactionManager"

      class="com.bodsite.common.datasource.DynamicDataSourceTransactionManager">

      <property name="dataSource" ref="dataSource" />

   </bean>

   <!-- 掃描@Transactional註解的類定義事務 -->

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

      proxy-target-class="true" />

   <!-- 自動註冊mybatis mapper bean -->

   <!-- 注意,沒有必要去指定SqlSessionFactory或SqlSessionTemplate, 由於MapperScannerConfigurer將會建立

      MapperFactoryBean,以後自動裝配。 可是,若是你使 用了一個以上的DataSource,那 麼自動裝配可能會失效。 這種狀況下,你能夠使用

      sqlSessionFactoryBeanName或sqlSessionTemplateBeanName 屬性來設置正確的 bean名稱來使用。 -->

   <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">

      <property name="basePackage" value="com.bodsite.**.dao" />

   </bean>

</beans>

1.1.六、mybatis-generator.xml 配置-添加攔截器

bodsite-site-service - mybatis- generator.xml

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

   <settings>

      <!--對在此配置文件下的全部cache 進行全局性開/關設置。 -->

      <setting name="cacheEnabled" value="true" />

      <!-- 全局性設置懶加載。若是設爲‘false’,則全部相關聯的都會被初始化加載。 -->

      <setting name="lazyLoadingEnabled" value="true" />

      <!-- 容許和不容許單條語句返回多個數據集(取決於驅動需求) -->

      <setting name="multipleResultSetsEnabled" value="true" />

      <!-- 使用列標籤代替列名稱。 -->

      <setting name="useColumnLabel" value="true" />

      <!-- 容許JDBC 生成主鍵。須要驅動器支持。若是設爲了true,這個設置將強制使用被生成的主鍵,有一些驅動器不兼容不過仍然能夠執行。 -->

      <setting name="useGeneratedKeys" value="false" />

      <!-- 這是默認的執行類型 (SIMPLE: 簡單; REUSE: 執行器可能重複使用prepared statements語句;BATCH:

         執行器能夠重複執行語句和批量更新) -->

      <setting name="defaultExecutorType" value="SIMPLE" />

      <!-- 配置Java屬性-數據庫表字段對應駝峯規則 -->

      <setting name="mapUnderscoreToCamelCase" value="true" />

   </settings>

   <plugins>

   <!-- 讀寫分離攔截器 -->

   <plugin interceptor="com.bodsite.common.datasource.DynamicDataSourceInterceptor">

   </plugin>

   </plugins>

</configuration>

1.二、測試

一、啓動bodsite-site-service,在bodsite-site中的DemoConsumer類,執行查詢

  

二、在bodsite-site中的DemoConsumer類,執行插入

項目地址:https://git.oschina.net/bodsite/bodsite

相關文章
相關標籤/搜索