讀寫分離若是撇開框架無非就是實現多個數據源,主庫用寫的數據源,從庫用讀的數據源。
由於想研究數據庫讀寫分離和分庫分表的設計,因此就本身搭建了一套springboot+druid+mybatis+aop 實現一主多從的設計。
第一步:首先須要自定義數據源的配置項,springboot默認解析的是帶前綴spring.datasource.下面的配置項,爲了避免衝突,就直接定義datasource.當成咱們的前綴,
@ConfigurationProperties(prefix = 「datasource.write」)能夠用來加載指定前綴的配置項,很是方便
由於使用druid,因此須要生成datasource的時候須要指定類型。java
DataSourceBuilder.create().type(dataSourceType).build()
readSize是用來定義從庫的大小,有多少從庫就要配置多少個從庫datasource
第二步:從庫的負載均衡,主要是MyAbstractRoutingDataSource這個類
第三步,從寫springboot-mybatis架包的MybatisAutoConfiguration類的建立SqlSessionFactory方法,將裏面的數據源換成咱們自定義的AbstractRoutingDataSource
第四步驟。自定義事務MyDataSourceTransactionManagerAutoConfigurationmysql
完整代碼和單元測試:
github:https://github.com/ggj2010/javabase.gitgit
主要的架包github
<!-- jdbc driver begin--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!--mybatis springboot--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <!-- jdbc driver end--> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>
自定義數據源配置項:spring
#多數據源 1主2從 datasource: #從庫數量 readSize: 2 # 使用druid數據源 type: com.alibaba.druid.pool.DruidDataSource #主庫 write: url: jdbc:mysql://localhost:3306/master?useUnicode=true&characterEncoding=utf-8 username: root password: root driver-class-name: com.mysql.jdbc.Driver filters: stat maxActive: 20 initialSize: 1 maxWait: 60000 minIdle: 1 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQueryTimeout: 900000 validationQuery: SELECT SYSDATE() from dual testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true maxOpenPreparedStatements: 20 read1: url: jdbc:mysql://localhost:3306/slave1?useUnicode=true&characterEncoding=utf-8 username: root password: root driver-class-name: com.mysql.jdbc.Driver filters: stat maxActive: 20 initialSize: 1 maxWait: 60000 minIdle: 1 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQueryTimeout: 900000 validationQuery: SELECT SYSDATE() from dual testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true maxOpenPreparedStatements: 20 read2: url: jdbc:mysql://localhost:3306/slave2?useUnicode=true&characterEncoding=utf-8 username: root password: root driver-class-name: com.mysql.jdbc.Driver filters: stat maxActive: 20 initialSize: 1 maxWait: 60000 minIdle: 1 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQueryTimeout: 900000 validationQuery: SELECT SYSDATE() from dual testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true maxOpenPreparedStatements: 20
解析配置項:sql
@Configuration @Slf4j public class DataSourceConfiguration { @Value("${datasource.type}") private Class<? extends DataSource> dataSourceType; @Bean(name = "writeDataSource") @Primary @ConfigurationProperties(prefix = "datasource.write") public DataSource writeDataSource() { log.info("-------------------- writeDataSource init ---------------------"); return DataSourceBuilder.create().type(dataSourceType).build(); } /** * 有多少個從庫就要配置多少個 * @return */ @Bean(name = "readDataSource1") @ConfigurationProperties(prefix = "datasource.read1") public DataSource readDataSourceOne() { log.info("-------------------- readDataSourceOne init ---------------------"); return DataSourceBuilder.create().type(dataSourceType).build(); } @Bean(name = "readDataSource2") @ConfigurationProperties(prefix = "datasource.read2") public DataSource readDataSourceTwo() { log.info("-------------------- readDataSourceTwo init ---------------------"); return DataSourceBuilder.create().type(dataSourceType).build(); } }
重寫SqlSessionFactory數據庫
@Configuration @AutoConfigureAfter({ DataSourceConfiguration.class }) @Slf4j public class MybatisConfiguration extends MybatisAutoConfiguration { @Value("${datasource.readSize}") private String dataSourceSize; @Bean public SqlSessionFactory sqlSessionFactorys() throws Exception { log.info("-------------------- 重載父類 sqlSessionFactory init ---------------------"); return super.sqlSessionFactory(roundRobinDataSouceProxy()); } /** * 有多少個數據源就要配置多少個bean * @return */ @Bean public AbstractRoutingDataSource roundRobinDataSouceProxy() { int size = Integer.parseInt(dataSourceSize); MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource(size); Map<Object, Object> targetDataSources = new HashMap<Object, Object>(); DataSource writeDataSource = SpringContextHolder.getBean("writeDataSource"); // 寫 targetDataSources.put(DataSourceType.write.getType(), SpringContextHolder.getBean("writeDataSource")); for (int i = 0; i < size; i++) { targetDataSources.put(i, SpringContextHolder.getBean("readDataSource" + (i + 1))); } proxy.setDefaultTargetDataSource(writeDataSource); proxy.setTargetDataSources(targetDataSources); return proxy; } }
本地線程全局變量springboot
public class DataSourceContextHolder { private static final ThreadLocal<String> local = new ThreadLocal<String>(); public static ThreadLocal<String> getLocal() { return local; } /** * 讀多是多個庫 */ public static void read() { local.set(DataSourceType.read.getType()); } /** * 寫只有一個庫 */ public static void write() { local.set(DataSourceType.write.getType()); } public static String getJdbcType() { return local.get(); } }
多數據源切換mybatis
public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource { private final int dataSourceNumber; private AtomicInteger count = new AtomicInteger(0); public MyAbstractRoutingDataSource(int dataSourceNumber) { this.dataSourceNumber = dataSourceNumber; } @Override protected Object determineCurrentLookupKey() { String typeKey = DataSourceContextHolder.getJdbcType(); if (typeKey.equals(DataSourceType.write.getType())) return DataSourceType.write.getType(); // 讀 簡單負載均衡 int number = count.getAndAdd(1); int lookupKey = number % dataSourceNumber; return new Integer(lookupKey); } }
enum類型負載均衡
public enum DataSourceType { read("read", "從庫"), write("write", "主庫"); @Getter private String type; @Getter private String name; DataSourceType(String type, String name) { this.type = type; this.name = name; } }
aop攔截設置本地線程變量
@Aspect @Component @Slf4j public class DataSourceAop { @Before("execution(* com.ggj.encrypt.modules.*.dao..*.find*(..)) or execution(* com.ggj.encrypt.modules.*.dao..*.get*(..))") public void setReadDataSourceType() { DataSourceContextHolder.read(); log.info("dataSource切換到:Read"); } @Before("execution(* com.ggj.encrypt.modules.*.dao..*.insert*(..)) or execution(* com.ggj.encrypt.modules.*.dao..*.update*(..))") public void setWriteDataSourceType() { DataSourceContextHolder.write(); log.info("dataSource切換到:write"); } }
自定義事務
@Configuration @EnableTransactionManagement @Slf4j public class MyDataSourceTransactionManagerAutoConfiguration extends DataSourceTransactionManagerAutoConfiguration { /** * 自定義事務 * MyBatis自動參與到spring事務管理中,無需額外配置,只要org.mybatis.spring.SqlSessionFactoryBean引用的數據源與DataSourceTransactionManager引用的數據源一致便可,不然事務管理會不起做用。 * @return */ @Bean(name = "transactionManager") public DataSourceTransactionManager transactionManagers() { log.info("-------------------- transactionManager init ---------------------"); return new DataSourceTransactionManager(SpringContextHolder.getBean("roundRobinDataSouceProxy")); } }