Because I want to study the design of database read-write separation and sub-database sub-table, so I built a set of springboot + druid + mybatis + aop to achieve a master-slave design.
The first step: First, you need to customize the configuration items of the data source. Springboot parses the default configuration items with the spring.datasource. By default , in order not to conflict, we directly define the datasource. As our prefix,
@ConfigurationProperties (prefix = " datasource.write ") can be used to load configuration item specified prefix, it is very convenient
because Druid use, it is necessary to generate the time required to specify the type datasource.
DataSourceBuilder.create().type(dataSourceType).build()
readSize is used to define the size of the library, how many from the library is to configure the number from the library datasource
Step two: from the load balancing pool, mainly MyAbstractRoutingDataSource this class
third step, from writing springboot-mybatis shelf package MybatisAutoConfiguration The class creates the SqlSessionFactory method, and replaces the data source in it with our custom AbstractRoutingDataSource
fourth step. Custom transaction MyDataSourceTransactionManagerAutoConfiguration
Complete code and unit testing:
github: https://github.com/ggj2010/javabase.git
Main rack package
<!-- 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>
Custom data source configuration items:
#多数据源 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
Parsing configuration items:
@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();
}
}
Rewrite 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;
}
}
Local thread global variables
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();
}
}
Multi-data source switching
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 type
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 interception set local thread variable
@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");
}
}
Custom transaction
@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"));
}
}