自定义注解之读写分离

前言

这段时间由于公司马上要做618的活动了,担心并发上来了,系统扛不住,所以这段时间都在做系统的优化,先开始做的是将大量的查询接口先落到从库上面去,降低主库请求次数,后面会由DBA会整理出那些慢SQL和执行次数较多的SQL,然后去优化sql和业务,这是后话了,这次主要是去给只读接口添加上自定义注解,让这些请求全部落到从库上去。闲来无事也看了一下自定义注解的实现方法,以及mybatis多数据源的实现方法。

开始

据说这个自定义注解和多数据源是架构组实现的,我先来探探究竟,我将这个自定义注解分离出来做成了一个小demo,可以说是呕心沥血,各种异常,各种缺bean,但最终还是能正常使用,直接看代码吧

代码

注解的定义
1
2
3
4
5
6
7
8
9
10
11
12
/***
* 自定义注解实现读数据源
* create by Congz
*
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface ReadDataSource {

}

首先定义一个注解 ReadDataSourcer上面有四个元注解。

@Target({ElementType.METHOD, ElementType.TYPE})表示了这个注解的可修饰范围,我只给他指定了作用在方法上,和作用在类上

@Retention(RetentionPolicy.RUNTIME)表示这个注解的有效时间,这个注解是动态切换数据源的,所以我给的是运行时有效

@Inherited 允许子类继承父类的注解,当一个类上面使用了该元注解的注解,表示那个类的子类也能继承该注解,有点绕,但是就是这样的

@Documented 表示允许javadoc生成文档

这样,注解就定义好了。

注解实现
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
/**
* @author CongZ
* @classname DataSourceControllerAop
* @create 2019/5/21
**/
@Aspect
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)
@Component
public class DataSourceControllerAop implements PriorityOrdered {


/**
* 使用aop切面,
* @param jp
*/
@Before("execution(* com.mybatis..*Controller.*(..))" +
" && (@annotation(com.mybatis.datasource.ReadDataSource))" +
" && (@annotation(org.springframework.web.bind.annotation.RequestMapping))")
public void setReadDataSourceBefore(JoinPoint jp) {
String rw = DataSourceContextHolder.getReadOrWrite();
if (!DataSourceType.write.getType().equals(rw)) {
//输出切点信息 便于问题跟踪
DataSourceContextHolder.setRead();
}
}

@After("execution(* com.mybatis..*Controller.*(..))" +
" && (@annotation(com.mybatis.datasource.ReadDataSource))" +
" && (@annotation(org.springframework.web.bind.annotation.RequestMapping))")
public void clearReadDataSourceAfter(JoinPoint jp) {
//方法执行完成后 清除线程上的读写key
DataSourceContextHolder.clear();
}
/**
* PriorityOrdered是个接口,继承自Ordered接口,未定义任何方法。
* 返回的order值越小,表示优先级越高
* @return
*/
@Override
public int getOrder() {
return 1;
}
}

这个注解是用的aop切面来实现的,定位到使用过这个注解的方法,然后织入进去,具体的AOP我会在下一章分析

这里就不谈一些切面的注解了,留在谈AOP的时候在去看看。

额,好像必须要谈一个@Before这注解,表示在方法执行前先调用这个方法,还有@After表示方法执行之后去调用这个方法

这里可以看到DataSourceContextHolder.getReadOrWrite()会去获取此时数据库上下文的读写状态,如果为写的话,则要改成读,具体源码在下面,先来说说原理

这里其实有一个疑点,当获取到读写状态时,应该都是为空的,因为放该方法执行完之后,会去clear一下,所以这里可以理解为,当上下文的读写状态为空时,则切换到从库上面去

然后@After就是当前方法执行之后需要去清空一下上下文的状态

可以看到这里会继承一个PriorityOrdered的类,然后实现getOrder方法,这个表示该事物的优先级别,返回的order值越小,表示优先级越高,还有继承了PriortityOrdered的接口比Ordered的接口优先级要高

好,这个讲完了,在看一下上下文的切换

读写切换
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
/**
* @author CongZ
* @classname 本地线程,用于数据源切换上下文
* @create 2019/5/21
**/

public class DataSourceContextHolder {

//线程本地环境
private static final ThreadLocal<String> local = new ThreadLocal<String>();

public static ThreadLocal<String> getLocal() {
return local;
}

/**
* 读库
*/
public static void setRead() {
local.set(DataSourceType.read.getType());
}

/**
* 写库
*/
public static void setWrite() {
local.set(DataSourceType.write.getType());
}


public static String getReadOrWrite() {
return local.get();
}

public static void clear() {
local.remove();
}
}

这里其实没什么,只是使用了ThreadLocal去维护变量,用这个的好处就是它会为每个线程提供一个独立的副本,所以每个线程都能独立的改变自己的副本,而不会影响其他线程所对应的副本

数据源的枚举
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
public enum DataSourceType {

read("read", "从库"),
write("write","主库");

private String type;

private String name;

DataSourceType(String type, String name) {
this.type = type;
this.name = name;
}

public String getType() {
return type;
}

public void setType(String type) {
this.type = type;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

就两个库,如果需要添加,继续添加枚举就好了

Druid配置

接下来的两个是重头戏,需要自定义配置两个东西,一个是Druid数据库连接池,第二个是Mybatis的配置

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
50
51
52
53
54
55
56
57
58
59
60
61
62
    @Value("${mysql.datasource.type}")
private Class<? extends DataSource> dataSourceType;


/**
* 写库 数据源配置
*
* @return
*/
@Bean(name = "writeDataSource")
@Primary
@ConfigurationProperties(prefix = "mysql.datasource.write")
public DataSource writeDataSource() {
log.info("-------------------- writeDataSource init ---------------------");
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
dataSourceBuilder.type(dataSourceType);
DruidDataSource druidDataSource = (DruidDataSource) dataSourceBuilder.build();
druidDataSource.setMaxActive(30);
druidDataSource.setInitialSize(5);
druidDataSource.setMaxWait(60000);
druidDataSource.setMinIdle(5);
druidDataSource.setTimeBetweenEvictionRunsMillis(60000);
druidDataSource.setMinEvictableIdleTimeMillis(1200000);
druidDataSource.setValidationQuery("select 1");
druidDataSource.setTestWhileIdle(true);
druidDataSource.setTestOnBorrow(true);
druidDataSource.setTestOnReturn(false);
druidDataSource.setPoolPreparedStatements(true);
druidDataSource.setMaxOpenPreparedStatements(20);

setFilters(druidDataSource);
return druidDataSource;
}

/**
* 有多少个从库就要配置多少个
*
* @return
*/
@Bean(name = "readDataSource01")
@ConfigurationProperties(prefix = "mysql.datasource.read01")
public DataSource readDataSourceOne() {
log.info("-------------------- read01 DataSourceOne init ---------------------");
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
dataSourceBuilder.type(dataSourceType);
DruidDataSource druidDataSource = (DruidDataSource) dataSourceBuilder.build();
druidDataSource.setMaxActive(30);
druidDataSource.setInitialSize(5);
druidDataSource.setMaxWait(60000);
druidDataSource.setMinIdle(5);
druidDataSource.setTimeBetweenEvictionRunsMillis(60000);
druidDataSource.setMinEvictableIdleTimeMillis(1200000);
druidDataSource.setValidationQuery("select 1");
druidDataSource.setTestWhileIdle(true);
druidDataSource.setTestOnBorrow(true);
druidDataSource.setTestOnReturn(false);
druidDataSource.setPoolPreparedStatements(true);
druidDataSource.setMaxOpenPreparedStatements(20);

setFilters(druidDataSource);
return druidDataSource;
}

这个相当于是配置Druid的数据源,这里有多少数据源就要配置多少数据源,然后会去Mybatis的配置里面动态切换。

这个@ConfigurationProperties(prefix = “mysql.datasource.write”):作用就是将 全局配置文件中 前缀为 mysql.datasource.write的属性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名参数中,然后下面一个read01同理

到这里我们的数据源,和动态切换都写好了,在就去配置Myabtis了,让他也能动态切换Druid里面配置的多个数据源

Mybatis自定义配置

在看这段代码的时候,着实下了一番功夫,不理解Mybatis的原理和sqlSessionFactory的用处导致很难懂

来看看代码

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/**
* @author CongZ
* @classname 自定义的Mybatis配置
* @create 2019/5/21
**/
@Configuration
@AutoConfigureAfter(DruidDoubleSourceConfiguration.class)
@MapperScan(basePackages = {"com.mybatis.demo.*.dao"})
public class MybatisConfiguration extends MybatisAutoConfiguration {

private static Logger log = LoggerFactory.getLogger(MybatisConfiguration.class);

@Value("${mysql.datasource.readSize}")
private String readDataSourceSize;


@Value("${mybatis.mapperLocations}")
private String mybatisMapperLocations;

@Autowired
@Qualifier("writeDataSource")
private DataSource writeDataSource;
@Autowired
@Qualifier("readDataSource01")
private DataSource readDataSource01;


public MybatisConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider, ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider, ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
super(properties, interceptorsProvider, resourceLoader, databaseIdProvider, configurationCustomizersProvider);
}

@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactorys() throws Exception {
log.info("-------------------- sqlSessionFactory init ---------------------");
try {
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
sessionFactoryBean.setDataSource(roundRobinDataSouceProxy());

//设置mapper.xml文件所在位置
Resource[] resources = new PathMatchingResourcePatternResolver().getResources(mybatisMapperLocations);
sessionFactoryBean.setMapperLocations(resources);
//设置字段和列对应 user_id -> userId
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
sessionFactoryBean.setConfiguration(configuration);
return sessionFactoryBean.getObject();
} catch (IOException e) {
log.error("mybatis resolver mapper*xml is error", e);
return null;
} catch (Exception e) {
log.error("mybatis sqlSessionFactoryBean create error", e);
return null;
}
}
/**
* 把所有数据库都放在路由中
* @return
*/
@Bean(name = "roundRobinDataSouceProxy")
public AbstractRoutingDataSource roundRobinDataSouceProxy() {

Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
//把所有数据库都放在targetDataSources中,注意key值要和determineCurrentLookupKey()中代码写的一至,
//否则切换数据源时找不到正确的数据源
targetDataSources.put(DataSourceType.write.getType(), writeDataSource);
targetDataSources.put(DataSourceType.read.getType() + "1", readDataSource01);
final int readSize = Integer.parseInt(readDataSourceSize);

//路由类,寻找对应的数据源
AbstractRoutingDataSource proxy = new AbstractRoutingDataSource() {
private AtomicInteger count = new AtomicInteger(0);

/**
* 这是AbstractRoutingDataSource类中的一个抽象方法,
* 而它的返回值是你所要用的数据源dataSource的key值,有了这个key值,
* targetDataSources就从中取出对应的DataSource,如果找不到,就用配置默认的数据源。
*/
@Override
protected Object determineCurrentLookupKey() {
String typeKey = DataSourceContextHolder.getReadOrWrite();
if (typeKey == null) {
return DataSourceType.write.getType();
}

if (typeKey.equals(DataSourceType.write.getType())) {
return DataSourceType.write.getType();
}
//读库, 简单负载均衡
int number = count.getAndAdd(1);
int lookupKey = number % readSize;
return DataSourceType.read.getType() + (lookupKey + 1);
}
};
proxy.setDefaultTargetDataSource(writeDataSource);//默认库
proxy.setTargetDataSources(targetDataSources);
return proxy;
}


@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}

//事务管理
@Bean
public PlatformTransactionManager annotationDrivenTransactionManager() {
return new DataSourceTransactionManager(roundRobinDataSouceProxy());
}
}

这个注解表示当spring加载完DruidDoubleSourceConfiguration.class这个类的时候在去加载Mybatis的自定义配置类,意思就是,先把数据源的配置加载完,在去使用这个配置

1
@AutoConfigureAfter(DruidDoubleSourceConfiguration.class)

这里就会使用到刚才配置的两个数据源了

1
2
3
4
5
6
@Autowired
@Qualifier("writeDataSource")
private DataSource writeDataSource;
@Autowired
@Qualifier("readDataSource01")
private DataSource readDataSource01;

然后我们需要继承MybatisAutoConfiguration这个类,这里明白了一个东西,就是当父类的构造方法有参数,或者有重载时,子类必须先通过super调用父类的构造方法,而且需要放在子类构造方法的第一行,不然报错

这里面的重点在于AbstractRoutingDataSource这个的实现,扩展这个类会实现里面的一个抽象方法determineCurrentLookupKey(),这个方法会返回数据源的一个key值,然后上面填充的targetDataSources

Map,当返回了key值之后,就会去匹配targetDataSources里面的key值,如果有,就使用,没有的话,就默认使用写库,就是主库,这里就会返回一个数据源

因为mybatis不能满足我们现有的需求,所以也重新写了一个sqlSeesionFactory里面会指定roundRobinDataSouceProxy返回的一个数据源。这个是创建sqlSession的实例工厂。

然后通过sqlSessionTemplate注入sqlSessionFactorry实例

总结

到这里就基本完成了自定义注解实现读写分离,代码不难,主要是思路,至少知道了自定义注解怎么写,怎么实现,以及mybatis的自定义配置和Druid的自定义配置

-------------本文结束感谢您的阅读-------------
0%