读写分离如果撇开框架无非就是实现多个数据源,主库用写的数据源,从库用读的数据源。
因为想研究数据库读写分离和分库分表的设计,所以就自己搭建了一套springboot+druid+mybatis+aop 实现一主多从的设计。
第一步:首先需要自定义数据源的配置项,springboot默认解析的是带前缀spring.datasource.下面的配置项,为了不冲突,就直接定义datasource.当成我们的前缀,
@ConfigurationProperties(prefix = “datasource.write”)可以用来加载指定前缀的配置项,非常方便
因为使用druid,所以需要生成datasource的时候需要指定类型。
DataSourceBuilder.create().type(dataSourceType).build()
readSize是用来定义从库的大小,有多少从库就要配置多少个从库datasource
第二步:从库的负载均衡,主要是MyAbstractRoutingDataSource这个类
第三步,从写springboot-mybatis架包的MybatisAutoConfiguration类的创建SqlSessionFactory方法,将里面的数据源换成我们自定义的AbstractRoutingDataSource
第四步骤。自定义事务MyDataSourceTransactionManagerAutoConfiguration
完整代码和单元测试:
github:https://github.com/ggj2010/javabase.git
主要的架包
<!-- 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>
自定义数据源配置项:
#多数据源 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
解析配置项:
@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;
}
}
本地线程全局变量
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();
}
}
多数据源切换
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() {
Object resultObject=null;
String typeKey = DataSourceContextHolder.getJdbcType();
//只对主库开启事务,如果typeKey为空表示获取主库的datasource
if (StringUtils.isEmpty(typeKey)){
resultObject= DataSourceType.write.getType();
}else{
// 读简单负载均衡
int number = count.getAndAdd(1);
int lookupKey = number % dataSourceNumber;
resultObject= new Integer(lookupKey);
}
log.info("determineCurrentLookupKey:"+resultObject);
return resultObject;
}
}
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*(..)) or execution(* com.ggj.encrypt.modules.*.dao..*.select*(..))")
public void setReadDataSourceType() {
DataSourceContextHolder.read();
log.info("dataSource切换到:Read");
}
}
自定义事务
@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"));
}
}