前言
随着业务的发展,系统用户访问量会不断的增加,为了使系统能够承受的住大量用户的并发访问,需要对系统进行优化。优化系统的方法有很多,数据库的读写分离是其中一种。
需求分析
为避免开发过程中不当的使用读写分离,导致程序异常或其他未知错误,需要满足以下要求:
- 通过注解方式基于Spring的AOP动态切换读写库
- 避免读数据源方法中操作写数据程序异常,如下:
@ReadDataSource
public Object read(Object record){
updateOne(Object record);
}
- 数据源读写切换不能影响事务注解的使用,即开启事务后切换数据源不会引起其他问题,伪代码如下:
@ReadDataSource
public Object read(Object record);
@Transactional
public void test(Object record){
mapper.read(record);
updateOther(record);
}
代码设计
1. 定义自定义注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface ReadDataSource {
}
2. 定义读写分离上下文标识(线程安全)
public class DataSourceContextHolder {
// 线程本地环境
private static final ThreadLocal<String> local = new ThreadLocal<String>();
/**
* 读库
*/
public static void setRead() {
local.set("read");
}
/**
* 写库
*/
public static void setWrite() {
local.set("write");
}
public static String getReadOrWrite() {
return local.get();
}
public static void clear() {
local.remove();
}
}
3. 配置数据源(读数据源、写数据源、动态路由数据源)
@Bean
@Primary
public DataSource writeDataSource() throws Exception {
return new DruidDataSource();
}
@Bean
public DataSource readDataSource() throws Exception {
return new DruidDataSource();
}
@Bean
public SqlSessionFactory sqlSessionFactorys(DataSource writeDataSource,DataSource readDataSource) throws Exception {
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
targetDataSources.put("write", writeDataSource);
targetDataSources.put("read", readDataSource);
AbstractRoutingDataSource proxy = new AbstractRoutingDataSource() {
@Override
protected Object determineCurrentLookupKey() {
String typeKey = DataSourceContextHolder.getReadOrWrite();
if (typeKey == null) {
return "write";
}
return typeKey;
}
};
proxy.setDefaultTargetDataSource(writeDataSource);// 默认库
proxy.setTargetDataSources(targetDataSources);
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
sessionFactoryBean.setDataSource(proxy);
return sessionFactoryBean.getObject();
}
4. 对读写数据源切换做AOP
1)切换读数据源放在mybatis的Mapper层,Mapper层是Interface,方法内无法编写内容。同时在且读数据源之前判断当前线程是否已经开启事务,已经开启事务则放弃切换读数据源,不会破坏当前事务。开启数据读数据源后方法执行结束手动切回写数据源,防止并发情况下当前线程被分配到执行写操作的代码,产生程序异常。
@Around("execution(* com.test.mapper..*.*(..))")
public Object setReadDataSourceType(ProceedingJoinPoint point) throws Throwable {
ReadDataSource annotation = null;
try {
//获取方法注解
MethodSignature methodSignature = (MethodSignature) point.getSignature();
annotation = methodSignature.getMethod().getAnnotation(ReadDataSource.class);
if(annotation != null){
try {
//获取当前事务
TransactionAspectSupport.currentTransactionStatus();
} catch (NoTransactionException e) {
//抛异常说明当前没有事务,可以切读库
DataSourceContextHolder.setRead();
}
}
return point.proceed();
} finally {
//当前为读库时,用完需要清除,使用默认写库
if(annotation != null
&& DataSourceContextHolder.getReadOrWrite() != null
&& DataSourceContextHolder.getReadOrWrite().equals("read")){
DataSourceContextHolder.clear();
}
}
}