由于项目越来越大,数据库的压力越来越大,之前只做了简单的数据库主从备份,没有进行读写分离,现在主库的压力越来越明显,除了需要优化SQL之外,减轻主库压力,读写分离是必不可少了,也是项目需要吧,在这里记录一下,也是供大家参考一下吧。
关键词:AOP、AbstractRoutingDataSource、动态切换数据源
废话不多说,首先在XML中进行读、写数据源的配置
<!--读数据源配置-->
<bean id="readDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init"destroy-method="close">
//配置省略
</bean>
<!--写数据源配置-->
<bean id="writeDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init"destroy-method="close">
//配置省略
</bean>
<!-- 动态数据源 -->
<bean id = "dataSource" class="com.xxx.xxx.DynamicDataSource" >
<!-- 已配置的数据源 -->
<property name="targetDataSources">
<map>
<entry key="READ" value-ref="readDataSource"/>
<entry key="WRITE" value-ref="writeDataSource"/>
</map>
</property>
<!-- 默认的数据源 -->
<property name="defaultTargetDataSource" ref="writeDataSource"/>
</bean>
具体的配置省略,建议加载资源文件,注意的点是,如何进行数据源的切换?通过动态数据源的的配置可以看出,其key就是负责切换数据源的,最最最重要一点:动态数据源
DynamicDataSource这个类一定要继承自:org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource,重写其determineCurrentLookupKey()方法。
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getType();
}
}
还需要一个存放key的地方--->>
DataSourceContextHolder工具类;为保证切换时线程安全我们使用
ThreadLocal来保存我们的key。
public class DataSourceContextHolder {
private static final Logger LOGGER = LoggerFactory.getLogger(DataSourceContextHolder.class);
public static final String DATA_SOURCE_WRITE = "WRITE";
public static final String DATA_SOURCE_READ = "READ";
// 线程本地环境
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
// 设置数据源类型
public static void setType(String type) {
if(LOGGER.isDebugEnabled())
LOGGER.debug("========>>>>>>>切换数据源,类型:"+type+"<<<<<<<<========");
contextHolder.set(type);
}
// 获取数据源类型
public static String getType() {
return (contextHolder.get());
}
// 清除数据源类型
public static void clearType() {
contextHolder.remove();
}
}
到这里其实现数据源的切换的核心部分已经完成,接下来,就需要实现怎么在特定的场景下去触发数据源切换,那么就交给Spring AOP来完成了。
@Component
@Aspect
public class DynamicDataSourceAspect {
@Pointcut("execution (* com.x.mapper.*.select*(..)) || execution (* com.x.mapper.*.count*(..)) ")
public void readMethodPointcut() {}
@Pointcut("execution(* com.mapper.*.insert*(..))||execution(* com.mapper.*.update*(..))||execution(* com.mapper.*.delete*(..))")
public void writeMethodPointcut() {}
@Before("readMethodPointcut()")
public void switchReadDataSource(){
//设置读数据源
DataSourceContextHolder.setType(DataSourceContextHolder.DATA_SOURCE_READ);
}
@Before("writeMethodPointcut()")
public void switchWriteDataSource(){
//设置写数据源
DataSourceContextHolder.setType(DataSourceContextHolder.DATA_SOURCE_WRITE);
}
}
PS:多谢一位朋友提醒,本来我考虑的是切到业务层,自己写个注解来切换,直接切到逆向工程的mapper更省事!
按照之前我的想法,会麻烦一点,思路大体就是,写一个注解@ReadSource,上面的切面切到业务层(或者是直接调用DAO层的逻辑层),对所有读数据的方法加上@ReadSource,切面里面获取方法的注解是否有@ReadSource,如果有则设置读数据源。想想都觉得后怕。。。。项目里几百上千个方法总是有的。。。。!!!
另外,很重要的一点!!!
另外,很重要的一点!!!
另外,很重要的一点!!!
直接切到mapper层面,对一些负责的业务,有可能会导致事务失效,很简单一个例子,一个业务即涉及读也涉及写,过程中发生了异常,由于事务针对同一数据源,很有可能导致回滚失败!!!所以之前想的方法虽然麻烦,但是在这种情况可还是可以避免掉事务失效的!!
两种方法各有利弊吧,看各自具体项目实际需求取舍了。