数据库主从分离通常有几类实现方式,一是在应用程序内区分主从,二是新增一层数据库代理服务器,在应用服务器和数据库服务器之间根据SQL区分主从。
前者的好处是可以不用新增额外的服务器开销,后者的好处是可以不用对项目程序逻辑做任何的改动。
而从应用程序内部区分主从,又有两种方式,一是根据包名区分,二是根据自定义注解区分。
根据包名区分大概就是把操作主库的Mapper和xml文件放到一类文件夹上,把查询从库的Mapper和xml文件放在另一个目录的文件夹下。
里面的关键点在于MasterDataSourceConfig和ClusterDataSourceConfig,这两个用于声明和扫描对于目录下的dao和mapper.xml文件,并且返回对于的数据库链接,具体代码如下:
package com.tlgg.druid;
import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;
/**
* 从库数据源配置
*/
@Configuration
// 扫描 Mapper 接口并容器管理
@MapperScan(basePackages = ClusterDataSourceConfig.PACKAGE, sqlSessionFactoryRef = "clusterSqlSessionFactory")
public class ClusterDataSourceConfig {
/**
* 精确到 cluster 目录,以便跟其他数据源隔离
*/
static final String PACKAGE = "com.tlgg.dao.cluster";
static final String MAPPER_LOCATION = "classpath:mapper/cluster/*.xml";
@Value("${cluster.datasource.url}")
private String url;
@Value("${cluster.datasource.username}")
private String user;
@Value("${cluster.datasource.password}")
private String password;
@Value("${cluster.datasource.driverClassName}")
private String driverClass;
/**
*获取数据库链接
*/
@Bean(name = "clusterDataSource")
public DataSource clusterDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driverClass);
dataSource.setUrl(url);
dataSource.setUsername(user);
dataSource.setPassword(password);
System.out.println("-------------cluster--------------------");
return dataSource;
}
/**
*事务管理
*/
@Bean(name = "clusterTransactionManager")
public DataSourceTransactionManager clusterTransactionManager() {
return new DataSourceTransactionManager(clusterDataSource());
}
/**
*初始化时,加载xml文件,生产sqlSessionFactory
*/
@Bean(name = "clusterSqlSessionFactory")
public SqlSessionFactory clusterSqlSessionFactory(@Qualifier("clusterDataSource") DataSource clusterDataSource)
throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(clusterDataSource);
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources(ClusterDataSourceConfig.MAPPER_LOCATION));
return sessionFactory.getObject();
}
}
还有一种方式就是通过自定义注解,通过AOP扫描该注解然后往ThreadLocal里面塞对应的主库处理/从库处理的属性,然后重写AbstractRoutingDataSource来实现返回对于的主从数据源来实现。
自定义注解如下:
package com.tlgg.db;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 声明使用主库从库的注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceSelect {
String dataSourceName() default "master";
}
往当前线程设置主库/从库处理的threadLocal类如下:
package com.tlgg.db;
public class HandleDataSource {
public static final ThreadLocal holder = new ThreadLocal();
/**
* 绑定当前线程数据源路由的key
* @param datasource
*/
public static void putDataSource(String datasource) {
holder.set(datasource);
}
/**
* 获取当前线程的数据源路由的key
* @return
*/
public static String getDataSource() {
return holder.get();
}
public static void close(){
holder.remove();
}
}
AOP切面类如下:
package com.tlgg.db;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.lang.reflect.Method;
@Aspect
@Component
@Slf4j
public class DataSourceAspect{
@Around("execution(public * com.tlgg..*.service..impl..*.*(..)) && @annotation(org.springframework.transaction.annotation.Transactional)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
try {
//如果当前线程没有设置过主库处理或者从库处理,那么就判断当前执行方法是否包含DataSourceSelect枚举
if (HandleDataSource.getDataSource() == null) {
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
DataSourceSelect dataSourceSelect = method.getAnnotation(DataSourceSelect.class);
//如果包含,则把当前线程的属性设为对于枚举的配置
if (dataSourceSelect != null) {
HandleDataSource.putDataSource(dataSourceSelect.dataSourceName());
} else {
//如果不包含,则默认为主库处理
HandleDataSource.putDataSource("master");
}
}
} catch (Throwable throwable) {
throw throwable;
} finally {
HandleDataSource.close();
}
return pjp.proceed();
}
}
最终在执行sql,获取数据库链接时,会调用我们重写的AbstractRoutingDataSource的类,根据当前线程里的属性来返回对应主库或者从库的链接
package com.tlgg.db;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class MultipleDataSource extends AbstractRoutingDataSource {
/**
* 获取与数据源相关的key
* 此key是Map resolvedDataSources 中与数据源绑定的key值
* 在通过determineTargetDataSource获取目标数据源时使用
*/
@Override
protected Object determineCurrentLookupKey() {
return HandleDataSource.getDataSource();
}
}
最后我们在需要使用的Service方法上声明就可以了:
package com.tlgg.loan.service.test.impl;
import com.cardniu.db.DataSourceSelect;
import org.springframework.stereotype.Service;
/**
* 测试service类
*/
@Service
public class DataSourceTestService {
@DataSourceSelect(dataSourceName = "master")
public void testMaster(){
//查询主库
}
@DataSourceSelect(dataSourceName = "slave")
public void testSlave(){
//查询从库
}
}
具体的数据库链接配置就省略了,有兴趣的同学可以留言探讨~