採用繼承DataSourceTransactionManager控制事務讀寫分離,以及mybatis攔截器控制方法讀寫分離,經過繼承AbstractRoutingDataSource獲取動態數據源。html
建立本地數據源類型管理類,使用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(); } } |
建立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) {
}
}
|
建立動態事務管理類,繼承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(); }
}
|
建立動態獲取數據源類,繼承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; }
}
|
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> |
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> |
一、啓動bodsite-site-service,在bodsite-site中的DemoConsumer類,執行查詢
二、在bodsite-site中的DemoConsumer類,執行插入
項目地址:https://git.oschina.net/bodsite/bodsite