18课:关于Springboot配置一写多读数据源切换
简介
公司应用积累一定时间之后数据库数据量会变多;可以考虑用到读写数据源;
也就是操作事物的处理直接操作写库;大概率的读功能则直接操作读库 ;区分开对应的功能;
项目demo下载
项目结构
代码简介
1.pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.khy</groupId>
<artifactId>multiple-data</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<!--含有多个main 需要指定某一个启动class类 -->
<start-class>com.khy.ApplicationMain</start-class>
</properties>
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.29</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
因为是demo 所以只配置了依赖的jar 如有需要可以自行添加
2.application.properties文件
server.name=demo
server.port=8080
datasource.type = com.alibaba.druid.pool.DruidDataSource
#write
datasource.write.name = up
datasource.write.url = jdbc:mysql://localhost:3306/data1?useUnicode=true&characterEncoding=utf-8&useSSL=false
datasource.write.username = root
datasource.write.password = khanyu
datasource.write.driver-class-name = com.mysql.jdbc.Driver
datasource.write.filters = stat
datasource.write.maxActive = 20
datasource.write.initialSize = 1
datasource.write.maxWait = 60000
datasource.write.minIdle = 1
datasource.write.timeBetweenEvictionRunsMillis = 60000
datasource.write.minEvictableIdleTimeMillis = 300000
datasource.write.validationQuery = select 'x'
datasource.write.testWhileIdle = true
datasource.write.testOnBorrow = false
datasource.write.testOnReturn = false
datasource.write.poolPreparedStatements = true
datasource.write.maxOpenPreparedStatements = 20
#read1
datasource.read1.name = up
datasource.read1.url = jdbc:mysql://localhost:3306/data2?useUnicode=true&characterEncoding=utf-8&useSSL=false
datasource.read1.username = root
datasource.read1.password = khanyu
datasource.read1.driver-class-name = com.mysql.jdbc.Driver
datasource.read1.filters = stat
datasource.read1.maxActive = 20
datasource.read1.initialSize = 1
datasource.read1.maxWait = 60000
datasource.read1.minIdle = 1
datasource.read1.timeBetweenEvictionRunsMillis = 60000
datasource.read1.minEvictableIdleTimeMillis = 300000
datasource.read1.validationQuery = select 'x'
datasource.read1.testWhileIdle = true
datasource.read1.testOnBorrow = false
datasource.read1.testOnReturn = false
datasource.read1.poolPreparedStatements = true
datasource.read1.maxOpenPreparedStatements = 20
#read2
datasource.read2.name = up
datasource.read2.url = jdbc:mysql://localhost:3306/data3?useUnicode=true&characterEncoding=utf-8&useSSL=false
datasource.read2.username = root
datasource.read2.password = khanyu
datasource.read2.driver-class-name = com.mysql.jdbc.Driver
datasource.read2.filters = stat
datasource.read2.maxActive = 20
datasource.read2.initialSize = 1
datasource.read2.maxWait = 60000
datasource.read2.minIdle = 1
datasource.read2.timeBetweenEvictionRunsMillis = 60000
datasource.read2.minEvictableIdleTimeMillis = 300000
datasource.read2.validationQuery = select 'x'
datasource.read2.testWhileIdle = true
datasource.read2.testOnBorrow = false
datasource.read2.testOnReturn = false
datasource.read2.poolPreparedStatements = true
datasource.read2.maxOpenPreparedStatements = 20
配置中有多个读库数据源都可以直接配置在当前文件中,只需要将对应数据库的服务器地址,用户名和密码更换成自己的即可
3.DataBaseConfiguration 配置文件内容
@Configuration
@MapperScan(basePackages={"com.khy.mapper"})
public class DataBaseConfiguration {
@Value("${datasource.type}")
private Class<? extends DataSource> dataSourceType;
@Bean(name="writeDataSource")
@Primary
@ConfigurationProperties(prefix = "datasource.write")
public DataSource writeDataSource() {
System.out.println("-------------------- writeDataSource init ---------------------");
DataSource dataSource = DataSourceBuilder.create().type(dataSourceType).build();
return dataSource;
}
/**
* 有多少个从库就要配置多少个
* @return
*/
@Bean(name = "readDataSource1")
@ConfigurationProperties(prefix = "datasource.read1")
public DataSource readDataSourceOne(){
System.out.println("-------------------- readDataSourceOne init ---------------------");
return DataSourceBuilder.create().type(dataSourceType).build();
}
@Bean(name = "readDataSource2")
@ConfigurationProperties(prefix = "datasource.read2")
public DataSource readDataSourceTwo() {
System.out.println("-------------------- readDataSourceTwo init ---------------------");
return DataSourceBuilder.create().type(dataSourceType).build();
}
/**
* 有多少个数据源都配置到当前类中
* 需要注意 targetDataSources map中的key 要和下面的readWriteKey 配置类中的key 一致
* @return
*/
@Bean(name="readWriteDataSource")
public ReadWriteDataSource readWriteDataSource(ReadWriteKey readWriteKey) {
ReadWriteDataSource dataSource = new ReadWriteDataSource();
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
targetDataSources.put("write", writeDataSource());
targetDataSources.put("read1", readDataSourceOne());
targetDataSources.put("read2", readDataSourceTwo());
dataSource.setTargetDataSources(targetDataSources );
dataSource.setDefaultTargetDataSource(writeDataSource());
dataSource.setReadWriteKey(readWriteKey);
System.out.println("-------------------- initDataSource init success ---------------------");
return dataSource;
}
/**
* 读写数据源切换的配置项
* @author khy
* @createTime 2021年1月20日上午10:25:16
* @return
*/
@Bean(name = "readWriteKey")
public ReadWriteKey initReadWriteKey() {
ReadWriteKey readWriteKey = new ReadWriteKey();
readWriteKey.setWriteKey("write");
Map<String, Integer> readSourceMap = new HashMap<>();
readSourceMap.put("read1", 1);//读1 权重1
readSourceMap.put("read2", 2);//读2 权重2
readWriteKey.setLoadBalance(new RoundRobinLoadBalance(readSourceMap));
return readWriteKey;
}
/**
* 设置对应的拦截
* @author khy
* @createTime 2021年1月19日上午11:09:32
* @param readWriteKey
* @return
*/
@Bean(name = "readWriteInterceptor")
public ReadWriteInterceptor initReadWriteInterceptor(ReadWriteKey readWriteKey) {
ReadWriteInterceptor readWriteInterceptor = new ReadWriteInterceptor();
readWriteInterceptor.setReadWriteKey(readWriteKey);
return readWriteInterceptor;
}
/**
* 默认事物拦截
* @author khy
* @createTime 2021年1月20日上午10:25:37
* @param transactionManager
* @param transactionAttributeSource
* @return
*/
@Bean(name = "transactionInterceptor")
public TransactionInterceptor initTransactionInterceptor(PlatformTransactionManager transactionManager,TransactionAttributeSource transactionAttributeSource) {
TransactionInterceptor transactionInterceptor = new TransactionInterceptor();
transactionInterceptor.setTransactionManager(transactionManager);
transactionInterceptor.setTransactionAttributeSource(transactionAttributeSource);
return transactionInterceptor;
}
@Bean
public BeanNameAutoProxyCreator beanNameAutoProxyCreator() {
BeanNameAutoProxyCreator beanNameAutoProxyCreator = new BeanNameAutoProxyCreator();
beanNameAutoProxyCreator.setBeanNames("*Service");//拦截的对象 需要在对应service上面@Service("xxxService") 否则拦截不生效
beanNameAutoProxyCreator.setInterceptorNames("readWriteInterceptor","transactionInterceptor");
beanNameAutoProxyCreator.setProxyTargetClass(true);
return beanNameAutoProxyCreator;
}
/** * mybatis 配置路径 */
private static String MYBATIS_CONFIG = "mybatis-config.xml";
@Resource(name="readWriteDataSource")
private DataSource readWriteDataSource;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(readWriteDataSource);
sqlSessionFactoryBean.setConfigLocation(new ClassPathResource(MYBATIS_CONFIG));
sqlSessionFactoryBean.setTypeAliasesPackage("com.khy.entity");
sqlSessionFactoryBean.getObject().getConfiguration().setMapUnderscoreToCamelCase(true);
return sqlSessionFactoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean(name = "transactionManager")
public org.springframework.jdbc.datasource.DataSourceTransactionManager transactionManagers() {
System.out.println("-------------------- transactionManager init ---------------------");
return new org.springframework.jdbc.datasource.DataSourceTransactionManager(readWriteDataSource);
}
}
里面包含多个数据库配置, 读写库切换的拦截器/事物拦截器/ 读库切换规则以及mybatis的配置内容;
4.ReadWriteInterceptor 读写库拦截
public class ReadWriteInterceptor implements MethodInterceptor {
private ReadWriteKey readWriteKey;
public ReadWriteInterceptor() {
}
/*
*主要通过service方法上的事物上面的readOnly 是否只读属性来处理
*/
public Object invoke(MethodInvocation invocation) throws Throwable {
//防止真实类被jdk和cglb代理获取不到真是类方法上面的注解内容;
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
String methodName = invocation.getMethod().getName();
Method method = targetClass.getMethod(methodName, invocation.getMethod().getParameterTypes());
Transactional tx = (Transactional) method.getAnnotation(Transactional.class);
if (tx == null) {
tx = (Transactional) targetClass.getAnnotation(Transactional.class);
}
if (tx != null && tx.readOnly()) {
this.readWriteKey.setReadKey();
} else {
this.readWriteKey.setWriteKey();
}
Object var5;
try {
var5 = invocation.proceed();
} catch (Throwable var9) {
throw var9;
} finally {
ShardingUtil.removeCurrent();//线程执行完毕之后清除
}
return var5;
}
public ReadWriteKey getReadWriteKey() {
return this.readWriteKey;
}
public void setReadWriteKey(ReadWriteKey readWriteKey) {
this.readWriteKey = readWriteKey;
}
}
5.ReadWriteDataSource 自定义的数据源
public class ReadWriteDataSource extends AbstractRoutingDataSource {
private ReadWriteKey readWriteKey;
@Override
protected Object determineCurrentLookupKey() {
String key = this.readWriteKey.getKey();
return key;
}
public ReadWriteKey getReadWriteKey() {
return readWriteKey;
}
public void setReadWriteKey(ReadWriteKey readWriteKey) {
this.readWriteKey = readWriteKey;
}
}
初始化sql
CREATE TABLE `t_user` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`user_name` varchar(20) DEFAULT NULL,
`age` int(2) DEFAULT NULL,
`money` decimal(10,2) DEFAULT NULL,
`birthday` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`score` int(2) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;
原理流程;
当调用到xxxService方法时会被我们配置的拦截器给拦截到; 在拦截器中;我们获取目标service方法上面的@Transactional 注解 或者class类上面的事物注解 判断注解中readOnly 属性是否是只读
当XXXService上面的 @Transactional
1.非只读 则表示走写库,根据 初始化ReadWriteKey 时设置的默认的key值进行判定 ,将write 写入到当前线程中去;后面获取对应数据源的时间通过ReadWriteDataSource中的determineCurrentLookupKey方法从我们设置到ThreadLocal中获取 ShardingHolder 获取对应的key;
2.只读 则表示走的读库,根据初始化ReadWriteKey 指定的是轮询的 RoundRobinLoadBalance 然后根据我们在map中设置的 读库的key和权重 然后从中获取指定key 的值存储到ThreadLocal中去;
注意点
spring 是支持jdk代理和cglb代理的
所以在拦截器中需要注意获取的一定是被代理真实对象才能获取到方法上面的注解
通过BeanNameAutoProxyCreator 设置拦截的Service 需要在对应的xxxService的类上面添加@Service(xxxService) 否则拦截器不生效…
因为默认@service注解 注入的bean 是根据class来注入的—
也可以调整BeanNameAutoProxyCreator 中拦截的 *ServiceImpl 也可以;
还有一点就是在同一个事物方法中 数据源也是不会切换的