讀寫分離常見有倆種方式java
1 第一種方式比較經常使用就是定義2個數據庫鏈接,一個是Master,另外一個是Slave。更新數據時咱們取Master,查詢數據時取Slave。太過簡單不作介紹。mysql
2 第二種方數據源式動態切換,將數據源動態織入到程序中,達到動態選擇讀取主庫仍是從庫的目的。主要使用的技術是:annotation,Spring AOP ,反射。下面介紹這種方式spring
首先建立DatabaseConfiguration
package com.testdatasource.third.configuration.datasource; import com.alibaba.druid.pool.DruidDataSource; import com.baomidou.mybatisplus.MybatisConfiguration; import com.baomidou.mybatisplus.entity.GlobalConfiguration; import com.baomidou.mybatisplus.mapper.LogicSqlInjector; import com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean; import com.testdatasource.common.enums.DatasourceType; import com.testdatasource.third.configuration.properties.DruidProperties; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.type.JdbcType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; @Configuration @EnableAutoConfiguration public class DatabaseConfiguration { @Autowired private ApplicationContext appContext; @Autowired private DruidProperties druidProperties; @Bean(name = "readDatasource") @Primary @ConfigurationProperties(prefix = "read.datasource") public DataSource readDatasource() { return getReadDruidDataSource(); } /** * 寫入數據源鏈接池配置 */ @Bean(name = "writeDatasource") @ConfigurationProperties(prefix = "write.datasource") public DataSource writeDatasource() { return getWriteDruidDataSource(); } private DruidDataSource getReadDruidDataSource() { DruidDataSource dataSource = new DruidDataSource(); druidProperties.configR(dataSource); return dataSource; } private DruidDataSource getWriteDruidDataSource() { DruidDataSource dataSource = new DruidDataSource(); druidProperties.configW(dataSource); return dataSource; } //初始化數據源bean,這個bean很重要,後面事務控制也會用到 @Bean public AbstractRoutingDataSource roundRobinDataSouceProxy(@Qualifier("readDatasource")DataSource read, @Qualifier("writeDatasource") DataSource write) { DynamicDataSource proxy = new DynamicDataSource(); Map<Object, Object> targetDataSources = new HashMap<Object, Object>(); targetDataSources.put( DatasourceType.read.getName(), read); targetDataSources.put(DatasourceType.write.getName(), write); proxy.setDefaultTargetDataSource(read); proxy.setTargetDataSources(targetDataSources); return proxy; } //初始化SqlSessionFactory,將自定義的多數據源ReadWriteSplitRoutingDataSource類實例注入到工廠中 @Bean public SqlSessionFactory sqlSessionFactory(@Qualifier("readDatasource")DataSource read, @Qualifier("writeDatasource") DataSource write) throws Exception { MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean(); sqlSessionFactory. setDataSource(this.roundRobinDataSouceProxy(read, write)); sqlSessionFactory.setMapperLocations( new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*Mapper.xml")); MybatisConfiguration configuration = new MybatisConfiguration(); configuration.setJdbcTypeForNull(JdbcType.NULL); configuration.setMapUnderscoreToCamelCase(true); configuration.setCacheEnabled(false); sqlSessionFactory.setConfiguration(configuration); sqlSessionFactory.setGlobalConfig(globalConfiguration()); return sqlSessionFactory.getObject(); } @Bean public GlobalConfiguration globalConfiguration() { GlobalConfiguration conf = new GlobalConfiguration(new LogicSqlInjector()); conf.setLogicDeleteValue("-1"); conf.setLogicNotDeleteValue("1"); conf.setIdType(0); conf.setDbColumnUnderline(true); conf.setRefresh(true); return conf; } }
建立 DataSourceContextHoldersql
package com.testdatasource.third.configuration.datasource; import com.testdatasource.common.enums.DatasourceType; /** * ClassName:DataSourceContextHolder * * @Description : 當前線程數據源 */ public class DataSourceContextHolder { /** * 默認數據源 */ public static final String DEFAULT_DS = DatasourceType.read.getName(); private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); // 設置數據源名 public static void setDB(String dbType) { contextHolder.set(dbType); } // 獲取數據源名 public static String getDB() { return (contextHolder.get()); } // 清除數據源名 public static void clearDB() { contextHolder.remove(); } }
建立 DynamicDataSource數據庫
package com.testdatasource.third.configuration.datasource; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * ClassName:DynamicDataSource * * @Description : 動態數據源 */ public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDB(); } }
建立 MybatisPlusConfigapache
package com.testdatasource.third.configuration.datasource; import com.baomidou.mybatisplus.plugins.PaginationInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * ClassName:MybatisPlusConfig * @Description : mybatis分頁插件 * @version */ @Configuration public class MybatisPlusConfig { /** * mybatis-plus分頁插件<br> */ @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); paginationInterceptor.setLocalPage(true);// 開啓 PageHelper 的支持 return paginationInterceptor; } }
建立 MyDataSourceTransactionManagerAutoConfigurationsession
package com.testdatasource.third.configuration.datasource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.sql.DataSource; /** * * 多數據源事物 */ @Configuration @EnableTransactionManagement public class MyDataSourceTransactionManagerAutoConfiguration extends DataSourceTransactionManagerAutoConfiguration { @Autowired private ApplicationContext appContext; /** * 自定義事務 * MyBatis自動參與到spring事務管理中,無需額外配置,只要org.mybatis.spring.SqlSessionFactoryBean引用的數據源與DataSourceTransactionManager引用的數據源一致便可,不然事務管理會不起做用。 * @return */ @Bean(name = "transactionManager") public DataSourceTransactionManager transactionManagers() { return new DataSourceTransactionManager((DataSource)appContext.getBean("roundRobinDataSouceProxy")); } }
數據源枚舉類mybatis
package com.testdatasource.common.enums; /** * ClassName:DatasourceType * * @Description :數據庫讀寫類型 * @version */ public enum DatasourceType { write("write"), read("read"); private String name; private DatasourceType(String name) { this.name = name(); } public String getName() { return this.name; } public void setName(String name) { this.name = name; } }
數據源配置類app
import com.alibaba.druid.pool.DruidDataSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.sql.SQLException; /** * ClassName:DruidProperties * * @Description : * druid鏈接池配置文件屬性,說明:這個類中包含了許多默認配置,若這些配置符合您的狀況,您能夠不用管,若不符合,建議不要修改本類,建議直接在"application.yml"中配置便可 * </p> */ @Component @ConfigurationProperties(prefix = "spring.datasource") public class DruidProperties { @Value("${spring.datasource.read_url}") private String rurl; @Value("${spring.datasource.read_username}") private String rusername; @Value("${spring.datasource.read_password}") private String rpassword; @Value("${spring.datasource.write_url}") private String wurl; @Value("${spring.datasource.write_username}") private String wusername; @Value("${spring.datasource.write_password}") private String wpassword; private String driverClassName = "com.mysql.jdbc.Driver"; private Integer initialSize = 2; private Integer minIdle = 1; private Integer maxActive = 20; private Integer maxWait = 60000; private Integer timeBetweenEvictionRunsMillis = 60000; private Integer minEvictableIdleTimeMillis = 300000; private String validationQuery = "SELECT 1 FROM DUAL"; private Boolean testWhileIdle = true; private Boolean testOnBorrow = false; private Boolean testOnReturn = false; private Boolean poolPreparedStatements = true; private Integer maxPoolPreparedStatementPerConnectionSize = 20; private String filters = "stat"; public void configR(DruidDataSource dataSource) { dataSource.setUrl(rurl); dataSource.setUsername(rusername); // dataSource.setPassword(AESUtil.decrypt(rpassword, null)); dataSource.setPassword(AESUtils.AESDecode(null,rpassword)); dataSource.setDriverClassName(driverClassName); dataSource.setInitialSize(initialSize); // 定義初始鏈接數 dataSource.setMinIdle(minIdle); // 最小空閒 dataSource.setMaxActive(maxActive); // 定義最大鏈接數 dataSource.setMaxWait(maxWait); // 最長等待時間 // 配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接,單位是毫秒 dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); // 配置一個鏈接在池中最小生存的時間,單位是毫秒 dataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); dataSource.setValidationQuery(validationQuery); dataSource.setTestWhileIdle(testWhileIdle); dataSource.setTestOnBorrow(testOnBorrow); dataSource.setTestOnReturn(testOnReturn); // 打開PSCache,而且指定每一個鏈接上PSCache的大小 dataSource.setPoolPreparedStatements(poolPreparedStatements); dataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize); try { dataSource.setFilters(filters); } catch (SQLException e) { e.printStackTrace(); } } public void configW(DruidDataSource dataSource) { dataSource.setUrl(wurl); dataSource.setUsername(wusername); dataSource.setPassword(AESUtils.AESDecode(null,wpassword)); dataSource.setDriverClassName(driverClassName); dataSource.setInitialSize(initialSize); // 定義初始鏈接數 dataSource.setMinIdle(minIdle); // 最小空閒 dataSource.setMaxActive(maxActive); // 定義最大鏈接數 dataSource.setMaxWait(maxWait); // 最長等待時間 // 配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接,單位是毫秒 dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); // 配置一個鏈接在池中最小生存的時間,單位是毫秒 dataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); dataSource.setValidationQuery(validationQuery); dataSource.setTestWhileIdle(testWhileIdle); dataSource.setTestOnBorrow(testOnBorrow); dataSource.setTestOnReturn(testOnReturn); // 打開PSCache,而且指定每一個鏈接上PSCache的大小 dataSource.setPoolPreparedStatements(poolPreparedStatements); dataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize); try { dataSource.setFilters(filters); } catch (SQLException e) { e.printStackTrace(); } } public String getRurl() { return rurl; } public void setRurl(String rurl) { this.rurl = rurl; } public String getRusername() { return rusername; } public void setRusername(String rusername) { this.rusername = rusername; } public String getRpassword() { return rpassword; } public void setRpassword(String rpassword) { this.rpassword = rpassword; } public String getWurl() { return wurl; } public void setWurl(String wurl) { this.wurl = wurl; } public String getWusername() { return wusername; } public void setWusername(String wusername) { this.wusername = wusername; } public String getWpassword() { return wpassword; } public void setWpassword(String wpassword) { this.wpassword = wpassword; } public String getDriverClassName() { return driverClassName; } public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; } public Integer getInitialSize() { return initialSize; } public void setInitialSize(Integer initialSize) { this.initialSize = initialSize; } public Integer getMinIdle() { return minIdle; } public void setMinIdle(Integer minIdle) { this.minIdle = minIdle; } public Integer getMaxActive() { return maxActive; } public void setMaxActive(Integer maxActive) { this.maxActive = maxActive; } public Integer getMaxWait() { return maxWait; } public void setMaxWait(Integer maxWait) { this.maxWait = maxWait; } public Integer getTimeBetweenEvictionRunsMillis() { return timeBetweenEvictionRunsMillis; } public void setTimeBetweenEvictionRunsMillis(Integer timeBetweenEvictionRunsMillis) { this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis; } public Integer getMinEvictableIdleTimeMillis() { return minEvictableIdleTimeMillis; } public void setMinEvictableIdleTimeMillis(Integer minEvictableIdleTimeMillis) { this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis; } public String getValidationQuery() { return validationQuery; } public void setValidationQuery(String validationQuery) { this.validationQuery = validationQuery; } public Boolean getTestWhileIdle() { return testWhileIdle; } public void setTestWhileIdle(Boolean testWhileIdle) { this.testWhileIdle = testWhileIdle; } public Boolean getTestOnBorrow() { return testOnBorrow; } public void setTestOnBorrow(Boolean testOnBorrow) { this.testOnBorrow = testOnBorrow; } public Boolean getTestOnReturn() { return testOnReturn; } public void setTestOnReturn(Boolean testOnReturn) { this.testOnReturn = testOnReturn; } public Boolean getPoolPreparedStatements() { return poolPreparedStatements; } public void setPoolPreparedStatements(Boolean poolPreparedStatements) { this.poolPreparedStatements = poolPreparedStatements; } public Integer getMaxPoolPreparedStatementPerConnectionSize() { return maxPoolPreparedStatementPerConnectionSize; } public void setMaxPoolPreparedStatementPerConnectionSize(Integer maxPoolPreparedStatementPerConnectionSize) { this.maxPoolPreparedStatementPerConnectionSize = maxPoolPreparedStatementPerConnectionSize; } public String getFilters() { return filters; } public void setFilters(String filters) { this.filters = filters; } }
annotation類做用於強制寫庫或者讀庫ide
package com.testdatasource.common.annotation; import com.testdatasource.common.enums.DatasourceType; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * ClassName:DS * @Description :自定義切換數據源註解 * @version */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface DS { DatasourceType value() default DatasourceType.write; }
切面類DynamicDataSourceAspect 用於輔助自定義切面註解切換數據源
package com.testdatasource.third.aspect; import com.testdatasource.common.annotation.DS; import com.testdatasource.third.configuration.datasource.DataSourceContextHolder; 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; /** * ClassName:DynamicDataSourceAspect * * @Description : 自定義切面註解切換數據源 * @version */ @Aspect @Component @Order(value = 2) public class DynamicDataSourceAspect { @Pointcut("@annotation(com.testdatasource.common.annotation.DS)") public void ds() { } @Before("ds()") public void beforeSwitchDS(JoinPoint point) { // 得到當前訪問的class Class<?> className = point.getTarget().getClass(); // 得到訪問的方法名 String methodName = point.getSignature().getName(); // 獲得方法的參數的類型 Class[] argClass = ((MethodSignature) point.getSignature()).getParameterTypes(); String dataSource = DataSourceContextHolder.DEFAULT_DS; try { // 獲得訪問的方法對象 Method method = className.getMethod(methodName, argClass); // 判斷是否存在@DS註解 if (method.isAnnotationPresent(DS.class)) { DS annotation = method.getAnnotation(DS.class); // 取出註解中的數據源名 dataSource = annotation.value().getName(); } } catch (Exception e) { e.printStackTrace(); } // 切換數據源 DataSourceContextHolder.setDB(dataSource); } @After("ds()") public void afterSwitchDS(JoinPoint point) { DataSourceContextHolder.clearDB(); } }
yml配置
spring: datasource: type: com.alibaba.druid.pool.xa.DruidXADataSource read_url: jdbc:mysql://localhost:3306/test?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&pinGlobalTxToPhysicalConnection=true read_username: root read_password: root write_url: jdbc:mysql://localhost:3306/test?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&pinGlobalTxToPhysicalConnection=true write_username: root write_password: root
好了 這樣配置就沒問題了。