读写分离自定义实现

背景

当SaaS平台发展到一定规模后,读写分离是必然的架构。尝试了目前流行的MyCat框架,但这个框架会损失不少性能。杀鸡焉用牛刀,于是就结合Spring自定义实现了读写分离功能,实现要点记录如下:

  1. 先实现一个读写分离Aspect,在事务拦截前执行,路由到读库还是写库。
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

package com.kingdee.re.common.dataSource;

import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.Ordered;
import org.springframework.transaction.annotation.Transactional;

import java.lang.reflect.Method;

/**
* 读写分离切面拦截,使用读数据源的2种情况:<p>
* 1)发现service中的方法有Transactional注解修饰时,且readOnly()==true时;<br>
* 2)当service的方法以get, select, query, find, export, fetch, is, page, search, count开头时,启用读数据源;<br>
* 不满足以上两种条件时,启用写数据源。
*
* <p>
* <b>注意:</b>实现Ordered接口是用来做切面拦截排序用的,当getOrder()越小越早执行,spring-mybatis.xml里定义<tx:annotation-driven>有用到,
* </p>
* @author fuqiang_wen 2018/7/17
*/

public class DataSourceAspect implements Ordered {
private final Logger logger = Logger.getLogger(DataSourceAspect.class);

public void before(JoinPoint point) {
Object target = point.getTarget();
String method = point.getSignature().getName();

Class<?>[] classz = target.getClass().getInterfaces();

Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
try {
Method m = classz[0].getMethod(method, parameterTypes);
if (m != null && m.isAnnotationPresent(Transactional.class)) {
Transactional annotation = m.getAnnotation(Transactional.class);
if(annotation.readOnly()){
DynamicDataSourceHolder.putDataSource(DynamicDataSourceHolder.DATA_SOURCE_READ);
}else{
DynamicDataSourceHolder.putDataSource(DynamicDataSourceHolder.DATA_SOURCE_WRITE);
}
}else if(guessIsReadMethod(m)){
DynamicDataSourceHolder.putDataSource(DynamicDataSourceHolder.DATA_SOURCE_READ);
} else {
DynamicDataSourceHolder.putDataSource(DynamicDataSourceHolder.DATA_SOURCE_WRITE);
}
} catch (Exception e) {
logger.error(e.getMessage());
logger.error("set dynamic datasource arise error", e);
}
}

/**
* 推测是否是只读方法,以方法名的第一个单词推测是,目前包含范围有:get, select, query, find, export, fetch, is, page, search, count
* @author fuqiang_wen 2018/7/26
* @param method
* @return
*/
private boolean guessIsReadMethod(Method method) {
String methodName = method.getName();
return methodName.startsWith("get") || methodName.startsWith("select") || methodName.startsWith("query")
|| methodName.startsWith("find") || methodName.startsWith("export") || methodName.startsWith("fetch")
|| methodName.startsWith("is") || methodName.startsWith("page") || methodName.startsWith("search")
|| methodName.startsWith("count");
}

@Override
public int getOrder() {
return 0;
}
}
  1. 添加一个动态数据源类DynamicDataSource, 从DynamicDataSourceHolder取对应的数据源,DynamicDataSourceHolder实现利用了ThreadLocal类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.kingdee.re.common.dataSource;

import org.apache.log4j.Logger;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;


/**
* 动态数据源,用来做读写分离
* @author fuqiang_wen 2018/7/17
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
private final Logger logger = Logger.getLogger(DynamicDataSource.class);

@Override
protected Object determineCurrentLookupKey() {
logger.debug("DynamicDataSource determineCurrentLookupKey");
return DynamicDataSourceHolder.getDataSource();
}
}
  1. 当前线程读库还是写库保持类DynamicDataSourceHolder, 动态数据源Holder, master是主库(写库),slave是从库(读库)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.kingdee.re.common.dataSource;

import org.apache.log4j.Logger;

/**
* 动态数据源Holder, 在DynamicDataSource引用到, 在DataSourceAspect设置值
* @author fuqiang_wen 2018/7/17
*/
public class DynamicDataSourceHolder {
private final static Logger logger = Logger.getLogger(DynamicDataSourceHolder.class);
public static final String DATA_SOURCE_WRITE = "master";
public static final String DATA_SOURCE_READ = "slave";

public static final ThreadLocal<String> holder = new ThreadLocal<String>();

public static void putDataSource(String name) {
holder.set(name);
}

public static String getDataSource() {
return holder.get();
}
}
  1. Spring配置好对应的Aspect,如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!-- service切入 配置数据库注解aop -->
    <bean id="manyDataSourceAspect" class="com.kingdee.re.common.dataSource.DataSourceAspect" />

    <aop:config>
    <aop:pointcut id="service" expression="execution(* com.kingdee.re.*.service.impl.*.*(..))" /><!--声明所有包含Service的类的所有方法使用事务-->

    <aop:advisor advice-ref="txAdvice" pointcut-ref="service" />

    <aop:aspect id="c" ref="manyDataSourceAspect">
    <aop:before pointcut-ref="service" method="before"/>
    </aop:aspect>
    </aop:config>