手动实现一下Mysql读写分离

这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

背景

当数据量过大时候,对单表进行更新、查询的操作有时候会导致锁表,让读写速度跟不上,一个页面就要2-3秒,这就需要使用读写分离。
很多应用的数据库读操作比写操作更加密集,而且查询条件相对复杂,数据库的大部分性能消耗在查询操作上了。为保证数据库数据的一致性,我们要求所有对于数据库的更新操作都是针对主数据库的,读操作从数据库来进行。

代码

配置文件

增加双数据源的数据库配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
yml复制代码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

1
2
3
4
5
java复制代码@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface DataSource {
String value() default "data1";
}
数据源key设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@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();
}

}
动态数据源类
1
2
3
4
5
6
java复制代码public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
数据源配置类

定义双数据源的key和bean对应关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
java复制代码@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进行切换不同数据源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码@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();
}
}
使用注解

在方法上面用自定义的数据源注解声明数据源,就可以实现不同方法,不同数据源调用。

1
2
3
4
java复制代码    @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>,里面应该保存当前所有可切换的数据源。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码
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;}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%