这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战
背景
当数据量过大时候,对单表进行更新、查询的操作有时候会导致锁表,让读写速度跟不上,一个页面就要2-3秒,这就需要使用读写分离。 很多应用的数据库读操作比写操作更加密集,而且查询条件相对复杂,数据库的大部分性能消耗在查询操作上了。为保证数据库数据的一致性,我们要求所有对于数据库的更新操作都是针对主数据库的,读操作从数据库来进行。
代码
配置文件
增加双数据源的数据库配置
spring:
datasource:
datasource1:
url: jdbc:mysql://localhost:3306/user?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&useAffectedRows=true
username: admin
password: 1024571
driver-class-name: com.mysql.jdbc.Driver
filters: stat,wall
initial-size: 1
min-idle: 1
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 30000
validation-query: SELECT 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
datasource2:
url: jdbc:mysql://localhost:3307/user?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&useAffectedRows=true
username: admin
password: 1024571
driver-class-name: com.mysql.jdbc.Driver
filters: stat,wall
initial-size: 1
min-idle: 1
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 30000
validation-query: SELECT 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
复制代码
自定义注解
自定义数据源key的注解,value为数据源key
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface DataSource {
String value() default "data1";
}
复制代码
数据源key设置
@Slf4j
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
// 设置数据源名称
public static void setDataSource(String dataSource){
contextHolder.set(dataSource);
}
public static String getDataSource(){
return contextHolder.get();
}
// 清除数据源
public static void clearDataSource(){
contextHolder.remove();
}
}
复制代码
动态数据源类
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
复制代码
数据源配置类
定义双数据源的key和bean对应关系
@Configuration
public class DataSourceConfig {
/**
* 数据源1
*/
@Bean(name = "data1")
@ConfigurationProperties(prefix = "spring.datasource.data1")
public DataSource Data1(){
return DataSourceBuilder.create().build();
}
/**
* 数据源2
*/
@Bean(name = "data2")
@ConfigurationProperties(prefix = "spring.datasource.data2")
public DataSource Data2(){
return DataSourceBuilder.create().build();
}
/**
* 数据源切换: 通过AOP在不同数据源之间动态切换
*/
@Primary
@Bean
public DataSource dynamicDataSource(){
DynamicDataSource dynamicDataSource = new DynamicDataSource();
//设置默认数据源
dynamicDataSource.setDefaultTargetDataSource(Data1());
//配置多数据源
Map<Object,Object> dsMap = new HashMap<>();
dsMap.put("data1",Data1());
dsMap.put("data2",Data2());
dynamicDataSource.setTargetDataSources(dsMap);
return dynamicDataSource;
}
/**
* 配置@Transactional注解事务
* @return
*/
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dynamicDataSource());
}
}
复制代码
自定义切面
切面实现方法通过注解中的value进行切换不同数据源。
@Aspect
@Component
public class DataSourceAspect {
@Pointcut("@annotation(com.wqy.data.annotation.DataSource)")
public void pointcutConfig(){
}
@Before("pointcutConfig()")
public void before(JoinPoint joinPoint){
//获得当前访问的class
Class<?> className = joinPoint.getTarget().getClass();
//获得访问的方法名
String methodName = joinPoint.getSignature().getName();
//得到方法的参数的类型
Class[] argClass = ((MethodSignature)joinPoint.getSignature()).getParameterTypes();
String dataSource = null;
try {
// 得到访问的方法对象
Method method = className.getMethod(methodName, argClass);
// 判断是否存在@DataSource注解
if (method.isAnnotationPresent(DataSource.class)) {
DataSource annotation = method.getAnnotation(DataSource.class);
// 取出注解中的数据源名
dataSource = annotation.value();
}
} catch (Exception e) {
e.printStackTrace();
}
// 设置数据源key
DataSourceContextHolder.setDataSource(dataSource);
}
@After("pointcutConfig()")
public void after(JoinPoint joinPoint){
DataSourceContextHolder.clearDataSource();
}
}
复制代码
使用注解
在方法上面用自定义的数据源注解声明数据源,就可以实现不同方法,不同数据源调用。
@DataSource("dataSource1")
public void queryUser() {
userMapper.select();
}
复制代码
原理解析
通过AOP对方法进行切面,将注解中的value获取到,并设置为数据源Key,通过数据源配置类,拿到数据源对应的数据库bean,进而实现数据源切换。
AbstractRoutingDataSource
AbstractRoutingDataSource
继承AbstractDataSource
,如果声明一个类DynamicDataSource
继承AbstractRoutingDataSource
后,DynamicDataSource
本身就相当于一种数据源。所以AbstractRoutingDataSource
必然有getConnection()
方法获取数据库连接。
大致流程为,通过determineCurrentLookupKey
方法获取一个key,通过key从resolvedDataSources
中获取数据源DataSource
对象。determineCurrentLookupKey()
是个抽象方法,需要继承AbstractRoutingDataSource
的类实现;而resolvedDataSources
是一个Map<Object, DataSource>
,里面应该保存当前所有可切换的数据源。
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;}
复制代码