基于mybatis的读写分离,多数据源自动路由

目录

一、我们为什么要这么做?

二、我们该怎么做?

三、用到的技术

四、使用

1、首先定义一个拦截器,在我们数据库操作之前进行拦截

2、然后在xml配置文件里面定义切面

3、我们获取到当前线程要执行sql的dao类的信息后放在ThreadLocal对象里面,等到选择路由的时候拿出来使用。


一、我们为什么要这么做?

  1. 在实际的高并发项目中,单库的压力非常大。这个时候需要引入数据库主从结构。(如果是分库分表或者是数据库集群,又另一说了)。
  2. 由于微服务没有拆分完全,或者压根就一个单应用,需要访问多个数据

二、我们该怎么做?

以前的做法可能是,在我们配置文件定义多个datasource,然后在dao中根据增、删、改、查去选择不同的datasource。这样做的话,可能就需要我们在代码里面硬编码选择数据源的过程,这样做显然不够友好,会产生很多冗余,重复的代码。

那有没有可以让我们的程序自动去选择路由数据源,而我代码中还是像以前那样,只关心业务逻辑,至于怎么选,选什么全都交给框架去实现。这样做的话,是不是瞬间感觉到代码清晰了很多了。那下面我们就来一步一步的实现看看。

三、用到的技术

  1. spring提供的 AbstractRoutingDataSource 类。该类就是在多数据源下,会根据determineCurrentLookupKey() 这个方法返回的路由key,在动态选择数据源
  2. spring AOP。需要在执行sql的方法前拦截请求,把该线程请求的方法,类名,参数等信息设置到线程局部变量(ThreadLocal)里面,然后determineCurrentLookupKey这个方法就可以根据线程的数据动态的选择数据源了。
  3. spring-boot-autoConfiguration。可以自动装载配置的bean。不需要再手工的去写配置文件

四、使用

1、首先定义一个拦截器,在我们数据库操作之前进行拦截

一下完整的所有代码都在我的github上,详见:https://github.com/terry2870/hp-springboot

 

定义 类 DAOMethodInterceptorHandle 实现 MethodInterceptor接口,完整代码如下

package com.hp.springboot.database.interceptor;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.hp.springboot.database.bean.DAOInterfaceInfoBean;
import com.hp.springboot.database.bean.DAOInterfaceInfoBean.DBDelayInfo;
import com.hp.springboot.threadprofile.profile.ThreadProfile;

/**
 * 
 * 描述:执行数据库操作之前拦截请求,记录当前线程信息
 * 之所以用抽象类,是因为可以扩展选择持久层框架。可以选择mybatis或jdbcTemplate,又或者hibernate
 * 作者:黄平
 * 时间:2018年4月11日
 */
public abstract class DAOMethodInterceptorHandle implements MethodInterceptor {

	private static Logger log = LoggerFactory.getLogger(DAOMethodInterceptorHandle.class);
	
	/**
	 * 存放当前执行线程的一些信息
	 */
	private static ThreadLocal<DAOInterfaceInfoBean> routeKey = new ThreadLocal<>();
	
	/**
	 * 最大数据库查询时间(超过这个时间,就会打印一个告警日志)
	 */
	private static final long MAX_DB_DELAY_TIME = 10L;
	
	/**
	 * 获取dao操作的对象,方法等
	 * @param invocation
	 * @return
	 */
	public abstract DAOInterfaceInfoBean getDAOInterfaceInfoBean(MethodInvocation invocation);
	
	/**
	 * 获取当前线程的数据源路由的key
	 */
	public static DAOInterfaceInfoBean getRouteDAOInfo() {
		return routeKey.get();
	}
	
	@Override
	public Object invoke(MethodInvocation invocation) throws Throwable {
		//获取dao的操作方法,参数等信息,并设置到线程变量里
		this.setRouteDAOInfo(getDAOInterfaceInfoBean(invocation));
		
		//设置进入查询,记录线程执行时长
		entry();
		Object obj = null;
		try {
			//执行实际方法
			obj = invocation.proceed();
			return obj;
		} catch (Exception e) {
			throw  e;
		} finally {
			//退出查询
			exit();
			
			//避免内存溢出,释放当前线程的数据
			this.removeRouteDAOInfo();
		}
	}
	
	/**
	 * 进入查询
	 */
	private void entry() {
		DAOInterfaceInfoBean bean = getRouteDAOInfo();
		//加入到我们的线程调用堆栈里面,可以统计线程调用时间
		ThreadProfile.enter(bean.getMapperNamespace(), bean.getStatementId());
		DBDelayInfo delay = bean.new DBDelayInfo();
		delay.setBeginTime(System.currentTimeMillis());
		bean.setDelay(delay);
	}
	
	/**
	 * 结束查询
	 */
	private void exit() {
		DAOInterfaceInfoBean bean = getRouteDAOInfo();
		DBDelayInfo delay = bean.getDelay();
		delay.setEndTime(System.currentTimeMillis());
		ThreadProfile.exit();
		//输出查询数据库的时间
		if (delay.getEndTime() - delay.getBeginTime() >= MAX_DB_DELAY_TIME) {
			log.warn("execute db expire time. {}", delay);
		}
		
	}
	
	/**
	 * 绑定当前线程数据源路由的key 使用完成后必须调用removeRouteKey()方法删除
	 */
	private void setRouteDAOInfo(DAOInterfaceInfoBean key) {
		routeKey.set(key);
	}

	/**
	 * 删除与当前线程绑定的数据源路由的key
	 */
	private void removeRouteDAOInfo() {
		routeKey.remove();
	}
}

这里通过getDAOInterfaceInfoBean这个方法获取当前线程调用的方法,参数,签名等一些信息。我这里提供了mybatis的实现。如下

/**
 * 
 */
package com.hp.springboot.mybatis.interceptor;

import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.util.ClassUtils;

import com.hp.springboot.database.bean.DAOInterfaceInfoBean;
import com.hp.springboot.database.interceptor.DAOMethodInterceptorHandle;

/**
 * @author huangping
 * Jul 14, 2020
 */
public class MyBatisDAOMethodInterceptorHandle extends DAOMethodInterceptorHandle {

	@Override
	public DAOInterfaceInfoBean getDAOInterfaceInfoBean(MethodInvocation invocation) {
		DAOInterfaceInfoBean bean = new DAOInterfaceInfoBean();
		
		// 获取当前类的信息(由于我们使用的mybatis,这里获取到的是spring的代理类信息)
		Class<?> clazz = invocation.getThis().getClass();
		
		// 这里获取的才是我们定义的dao接口对象
		Class<?>[] targetInterfaces = ClassUtils.getAllInterfacesForClass(clazz, clazz.getClassLoader());
		
		// 获取该类的父类(该操作暂时没用使用到)
		Class<?>[] parentClass = targetInterfaces[0].getInterfaces();
		if (ArrayUtils.isNotEmpty(parentClass)) {
			bean.setParentClassName(parentClass[0]);
		}
		
		// 设置类名信息
		bean.setClassName(targetInterfaces[0]);
		
		// 设置方法的类信息
		bean.setMapperNamespace(targetInterfaces[0].getName());
		
		// 设置方法名
		bean.setStatementId(invocation.getMethod().getName());
		
		// 设置方法参数
		bean.setParameters(invocation.getArguments());
		return bean;
	}
}

2、然后在xml配置文件里面定义切面

基于mybatis的读写分离,多数据源自动路由

这样,拦截器就生效了

 


3、我们获取到当前线程要执行sql的dao类的信息后放在ThreadLocal对象里面,等到选择路由的时候拿出来使用。

好,我们定义一个自动路由,执行步骤如下

  1. 服务启动时,加载数据库配置信息,读取主从数据库配置信息
  2. 所有数据源信息加载到 AbstractRoutingDataSource 的 targetDataSources中,以供后面动态选择
  3. 执行sql前,动态选择数据源路由

先选择是哪个数据库,再选择主从。动态选择路由步骤:

  1. 获取前面拦截器拦截的当前线程执行的方法信息
  2. 按照dao的className,从数据源中获取数据源(就是databases.yml里面有没有单独为该dao配置数据源)
  3. 如果有,则使用指定的数据源;如果没有,则使用默认(也就是第一个数据源)数据源
  4. 由于前面获取的只是数据源的前缀,下面我们还要获取真正的数据源对应的key
  5. 根据查询的方法名称,判断走读库,还是写库。(这里面会考虑方法前有没有加ForceMaster的注解)
  6. 返回最终真实的数据源的key,获取到真实的Datasource

对应代码如下:

/**
 * 
 */
package com.hp.springboot.database.datasource;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import com.hp.springboot.database.bean.DAOInterfaceInfoBean;
import com.hp.springboot.database.bean.DatabaseConfigProperties;
import com.hp.springboot.database.bean.DatabaseConfigProperties.DatabaseConfig;
import com.hp.springboot.database.bean.DynamicDatasourceBean;
import com.hp.springboot.database.datasource.pool.AbstConnectionPoolFactory;
import com.hp.springboot.database.enums.ConnectionPoolFactoryEnum;
import com.hp.springboot.database.exception.DataSourceNotFoundException;
import com.hp.springboot.database.exception.DynamicDataSourceRouteException;
import com.hp.springboot.database.interceptor.DAOMethodInterceptorHandle;
import com.hp.springboot.database.interceptor.ForceMasterInterceptor;

/**
 * 描述:动态路由选择数据源
 * 作者:黄平
 * 时间:2018年4月1日
 */
public class DynamicDatasource extends AbstractRoutingDataSource {

	
	private static Logger log = LoggerFactory.getLogger(DynamicDatasource.class);
	
	/**
	 * 存放所有的dao对应的数据源的key
	 * key=dao名称,value=databaseName
	 * 多数据源时,根据database.yml中的配置,先找有没有该dao指定的数据源,如果有,则使用指定的数据源,如果找不到,则使用第一个(也就是主数据源)数据源
	 */
	private static Map<String, String> databaseNameMap = new HashMap<>();
	
	/**
	 * 存放所有的数据源主从的个数
	 * master_databaseName,10
	 * slave_databaseName,20
	 */
	private static Map<String, Integer> databaseIPCountMap = new HashMap<>();
	
	/**
	 * 默认的数据源名称
	 */
	private static String DEFAULT_DATABASE_NAME = "";
	
	/**
	 * master数据源名称的前缀
	 */
	private static final String MASTER_DS_KEY_PREX = "master_";
	
	/**
	 * slave数据源名称的前缀
	 */
	private static final String SLAVE_DS_KEY_PREX = "slave_";
	
	/**
	 * 匹配查询语句
	 */
	private static Pattern select = Pattern.compile("^select.*");
	
	/**
	 * 匹配更新语句
	 */
	private static Pattern update = Pattern.compile("^update.*");
	
	/**
	 * 匹配插入语句
	 */
	private static Pattern insert = Pattern.compile("^insert.*");
	
	/**
	 * 匹配删除语句
	 */
	private static Pattern delete = Pattern.compile("^delete.*");
	
	/**
	 * 数据库配置信息
	 */
	private DatabaseConfigProperties databaseConfigProperties;
	
	public DynamicDatasource() {}
	
	public DynamicDatasource(DatabaseConfigProperties databaseConfigProperties) {
		this.databaseConfigProperties = databaseConfigProperties;
	}
	
	@Override
	public void afterPropertiesSet() {
		//设置targetDataSources 值
		if (databaseConfigProperties == null || CollectionUtils.isEmpty(databaseConfigProperties.getDatabaseConfigList())) {
			// 没有数据库配置信息,直接抛异常,启动失败
			log.error("set DynamicDatasource error. with databaseConfigProperties is null.");
			throw new DynamicDataSourceRouteException("DynamicDatasource route error. with databaseConfigProperties is null");
		}
		try {
			Map<Object, Object> targetDataSources = new HashMap<>();
			
			// 使用哪种类型的连接池(可以dbcp,Druid等等)
			AbstConnectionPoolFactory connectionPool = ConnectionPoolFactoryEnum.getConnectionPoolFactory(databaseConfigProperties.getPoolName());
			DynamicDatasourceBean dynamicDatasourceBean = null;
			String databaseName = null;
			
			// 循环遍历数据库配置信息
			for (DatabaseConfig databaseConfig : databaseConfigProperties.getDatabaseConfigList()) {
				if (databaseConfig.getServers() == null || CollectionUtils.isEmpty(databaseConfig.getServers().getMaster())) {
					// 没有配置数据库ip信息或没有配置主库,直接抛异常,启动失败
					log.error("init database error. with masterUrls is empty.");
					throw new DynamicDataSourceRouteException("masterUrls is empty. with databaseConfig is: " + databaseConfig);
				}
				
				databaseName = databaseConfig.getDatabaseName();
				dynamicDatasourceBean = connectionPool.getDynamicDatasource(databaseConfig);
				
				if (dynamicDatasourceBean == null || CollectionUtils.isEmpty(dynamicDatasourceBean.getMasterDatasource())) {
					log.error("init database error. with masterUrls is empty.");
					throw new DynamicDataSourceRouteException("masterUrls is empty. with databaseConfig is: " + databaseConfig);
				}
				
				//设置master
				for (int i = 0; i < dynamicDatasourceBean.getMasterDatasource().size(); i++) {
					// 设置到自动路由的map中
					targetDataSources.put(buildMasterDatasourceKey(databaseName, i), dynamicDatasourceBean.getMasterDatasource().get(i));
				}
				//设置master有几个数据源
				databaseIPCountMap.put(buildMasterDatasourceKey(databaseName, -1), dynamicDatasourceBean.getMasterDatasource().size());
				
				//设置slave
				if (CollectionUtils.isNotEmpty(dynamicDatasourceBean.getSlaveDatasource())) {
					for (int i = 0; i < dynamicDatasourceBean.getSlaveDatasource().size(); i++) {
						targetDataSources.put(buildSlaveDatasourceKey(databaseName, i), dynamicDatasourceBean.getSlaveDatasource().get(i));
					}
					//设置slave有几个数据源
					databaseIPCountMap.put(buildSlaveDatasourceKey(databaseName, -1), dynamicDatasourceBean.getSlaveDatasource().size());
				}
				
				//默认数据源
				if (StringUtils.isEmpty(DEFAULT_DATABASE_NAME)) {
					// databases.yml的节点 databaseConfigList 下的第一个数据源就是主数据源
					DEFAULT_DATABASE_NAME = databaseName;
				}
				
				//处理dao(这里就是多数据源自动路由使用)
				dealDAOS(databaseConfig.getDaos(), databaseName);
			}
			
			super.setTargetDataSources(targetDataSources);
			super.afterPropertiesSet();
		} catch (Exception e) {
			log.error("deal DynamicDatasource error.", e);
		}
	}

	@Override
	protected Object determineCurrentLookupKey() {
		// 获取当前线程的信息
		DAOInterfaceInfoBean daoInfo = DAOMethodInterceptorHandle.getRouteDAOInfo();
		if (daoInfo == null) {
			//如果没有获取到拦截信息,则取主数据库
			log.warn("determineCurrentLookupKey error. with daoInfo is empty.");
			//return null;
			// 由于上面没有设置defaultTargetDataSource,所以这里需要new一个对象出来空对象,下面会自动在主库中随机选择一个
			daoInfo = new DAOInterfaceInfoBean();
		}
		
		//按照dao的className,从数据源中获取数据源
		String mapperNamespace = daoInfo.getMapperNamespace();
		String databaseName = databaseNameMap.get(mapperNamespace);
		if (StringUtils.isEmpty(databaseName)) {
			//如果没有,则使用默认数据源
			databaseName = DEFAULT_DATABASE_NAME;
		}
		
		// 根据数据源的key前缀,获取真实的数据源的key
		// 这里考虑了在代码里面的注解(ForceMaster)
		String result = getDatasourceByKey(databaseName, getForceMaster(daoInfo));
		log.debug("-------select route datasource with statementId={} and result is {}", (daoInfo.getMapperNamespace() + "." + daoInfo.getStatementId()), result);
		return result;
	}
	
	/**
	* @Title: getForceMaster  
	* @Description: 获取是否  forceMaster
	* @param daoInfo
	* @return
	 */
	private boolean getForceMaster(DAOInterfaceInfoBean daoInfo) {
		if (ForceMasterInterceptor.getForceMaster()) {
			//有设置forceMaster
			return true;
		}
		
		return getMasterOrSlave(daoInfo);
	}
	
	/**
	* @Title: getMasterOrSlave  
	* @Description: 根据方法名,判断走读库还是写库
	* 这里约定我们的dao里面的方法命名
	* 查询方法:selectXXX
	* 新增方法:insertXXX
	* 更新方法:insertXXX
	* 删除方法:deleteXXX
	* 如果不符合这个规范,则默认路由到master库
	* @param daoInfo
	* @return
	 */
	private boolean getMasterOrSlave(DAOInterfaceInfoBean daoInfo) {
		//根据方法名称去判断
		boolean fromMaster = false;
		//获取用户执行的sql方法名
		String statementId = daoInfo.getStatementId();
		if (StringUtils.isEmpty(statementId)) {
			//没有获取到方法,走master
			return true;
		}
		statementId = statementId.toLowerCase();
		if (select.matcher(statementId).matches()) {
			// 如果是查询语句,这里,随机取主从
			int i = RandomUtils.nextInt(0, 2);
			fromMaster = BooleanUtils.toBoolean(i);
		} else if (update.matcher(statementId).matches() || insert.matcher(statementId).matches() || delete.matcher(statementId).matches()) {
			// 更新,插入,删除使用master数据源
			fromMaster = true;
		} else {
			//如果statemenetId不符合规范,则告警,并且使用master数据源
			log.warn("statement id {}.{} is invalid, should be start with select*/insert*/update*/delete*. ", daoInfo.getMapperNamespace(), daoInfo.getStatementId());
			fromMaster = true;
		}
		return fromMaster;
	}
	
	/**
	* @Title: getDatasourceByKey  
	* @Description: 随机获取路由
	* @param databaseName
	* @param fromMaster
	* @return
	 */
	private String getDatasourceByKey(String databaseName, boolean fromMaster) {
		String datasourceKey = null;
		Integer num = null;
		if (fromMaster) {
			datasourceKey = buildMasterDatasourceKey(databaseName, -1);
			num = databaseIPCountMap.get(datasourceKey);
			if (num == null) {
				//没找到,直接抛出异常
				log.error("datasource not found with databaseName= {}", databaseName);
				throw new DataSourceNotFoundException(databaseName);
			}
		} else {
			datasourceKey = buildSlaveDatasourceKey(databaseName, -1);
			num = databaseIPCountMap.get(datasourceKey);
			if (num == null) {
				//没有配置从库,则路由到主库
				return getDatasourceByKey(databaseName, true);
			}
		}
		
		int random = 0;
		if (num == 1) {
			//如果就只有一个数据源,则就选择它
			random = 0;
		} else {
			//随机获取一个数据源
			random = RandomUtils.nextInt(0, num);
		}
		return fromMaster ? buildMasterDatasourceKey(databaseName, random) : buildSlaveDatasourceKey(databaseName, random);
	}
	
	/**
	* @Title: dealDAOS  
	* @Description: dao处理
	* @param daoList
	* @param databaseName
	 */
	private void dealDAOS(List<String> daoList, String databaseName) {
		if (CollectionUtils.isEmpty(daoList)) {
			return;
		}
		for (String dao : daoList) {
			databaseNameMap.put(dao, databaseName);
		}
	}

	/**
	* @Title: buildMasterDatasourceKey  
	* @Description: 获取主数据源的key
	* @param databaseName
	* @param index
	* @return
	 */
	private String buildMasterDatasourceKey(String databaseName, int index) {
		StringBuilder sb = new StringBuilder(MASTER_DS_KEY_PREX).append(databaseName);
		if (index >= 0) {
			sb.append("_").append(index);
		}
		return sb.toString();
	}
	
	/**
	 * 获取从数据源的key
	 * @param databaseName
	 * @param index
	 * @return
	 */
	private String buildSlaveDatasourceKey(String databaseName, int index) {
		StringBuilder sb = new StringBuilder(SLAVE_DS_KEY_PREX).append(databaseName);
		if (index >= 0) {
			sb.append("_").append(index);
		}
		return sb.toString();
	}

}

这样,我们就根据方法名称,自动路由到对应的数据库中。我们的业务代码还是跟原来一样写,对我们业务代码几乎没有侵入性。

 

你以为这样就大功告成了吗?或许上面在大部分情况下都已经没有问题了。但是,敲黑板,敲黑板,我们还没有考虑到事务的情况

我们正常如果要加事务的时候,就会在方法前加上@Transactional注解。如下所示:

基于mybatis的读写分离,多数据源自动路由

由于Transactional是加在方法上的,所以在进入这个test()方法之前,spring就要选择好数据源,但是我们DAOMethodInterceptorHandle拦截器拦截的是DAO的方法,dao的方法在service里面,所以这个时候你会发现,spring执行determineCurrentLookupKey()这个方法会获取不到数据源(当然,我这里面写了,默认返回了第一个数据源),因为我们还没有执行拦截器设置线程变量。所以,这个时候如果再执行sql,就会报错(准确的说,执行非默认数据库下的sql会报错)。

这个时候如果我们想使用非默认数据库下,而且又要有事务控制,这里就会报错。那问题来了,怎么解决呢?

我们正常使用自动路由的时候,是首先进入拦截器设置线程变量,再进入动态路由选择数据源。但是加了Transactional注解后,选择动态路由的步骤被强行提前到了service方法前了。所以这个时候是获取不到线程变量的。所以,在执行非默认数据库下,而且又要有事务控制,就会报错。

既然我们已经知道了执行逻辑和顺序,那问题就迎刃而解了。只要我们再加一个注解(跟Transactional平行),强制设置数据库。(不要妄想在一个事务下查询不同的数据库)

好,那我们加个注解 @UseDatabase

package com.hp.springboot.database.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 描述:强制使用数据库
 * 作者:黄平
 * 时间:2021-1-7
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
public @interface UseDatabase {

	/**
	* @Title: value  
	* @Description: 数据名称
	* @return
	 */
	String value() ;
}

解析UseDatabase注解:

package com.hp.springboot.database.interceptor;

import java.lang.reflect.Method;

import org.apache.commons.lang3.StringUtils;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;

import com.hp.springboot.database.annotation.UseDatabase;
import com.hp.springboot.database.exception.DatabaseNotSetException;

/**
 * 描述:强制使用数据
 * 作者:黄平
 * 时间:2021-1-7
 */
@Aspect
@Order(Ordered.HIGHEST_PRECEDENCE)
public class UseDatabaseInterceptor {

	private static Logger log = LoggerFactory.getLogger(UseDatabaseInterceptor.class);
	
	private static final ThreadLocal<String> USE_DATABASE = new InheritableThreadLocal<>();
	
	/**
	* @Title: around  
	* @Description: 设置强制走的数据库
	* @param joinPoint
	* @return
	* @throws Throwable
	 */
	@Around("@annotation(com.hp.springboot.database.annotation.UseDatabase)")
	public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
		log.debug("start before");
		
		//方法签名
		MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
		
		Method method = methodSignature.getMethod();
		
		// 获取UseDatabase注解
		UseDatabase useDatabase = method.getAnnotation(UseDatabase.class);
		
		// 设置的数据库名称
		String databaseName = useDatabase.value();
		
		if (StringUtils.isEmpty(databaseName)) {
			// 没有指定数据库名称,报错
			log.error("UseDatabase error. with databaseName is empty");
			throw new DatabaseNotSetException();
		}
		
		// 设置到线程变量中
		USE_DATABASE.set(databaseName);
		Object obj = null;
		try {
			obj = joinPoint.proceed();
			return obj;
		} catch (Exception e) {
			throw e;
		} finally {
			log.debug("start after");
			USE_DATABASE.remove();
		}
	}
	
	/**
	* @Title: getForceMaster  
	* @Description: 获取设置的哪个数据库
	* @return
	 */
	public static String getDatabaseName() {
		return USE_DATABASE.get();
	}
}

该拦截器类来解析我们加了UseDatabase注解的方法,并且把数据库名称设置到线程变量里面

注意这里加了基于mybatis的读写分离,多数据源自动路由

加了Order注解,并且设置注解顺序为最高优先级,来设置该拦截器执行顺序。不然这个拦截器如果在Transactional拦截器后面执行那就杯具了。

好,那我们的自动路由的determineCurrentLookupKey方法要做修改了

基于mybatis的读写分离,多数据源自动路由

当然,我们可以把这些包装在一个 springboot-starter里面,这样外面使用的话,直接maven依赖一下就可以了

ok大功告成!也解决事务问题。但是有一点要特别注意的,在@Transactional注解的方法内,不要去访问不同的数据库。

最后配上对应的databases.yml文件,该文件默认放在src/main/resources下面

hp:
  springboot:
    database:
      expression: "execution(* com.test.dal..*.*(..))"
      poolName: DBCP
      databaseConfigList:
        - databaseName: test
          servers:
            master:
              - ${database.test.master.url}
            slave:
              - 127.0.0.22:3307
              - 127.0.0.33:3308
          username: root
          password: 123456
    
        - databaseName: sys
          servers:
            master:
              - 127.0.0.1:3306
            slave:
              - 127.0.0.2:3301
              - 127.0.0.3:3302
          username: root
          password: 123456
          daos:
            - com.test.dal.ISysConfigDAO

 

基于mybatis的读写分离,多数据源自动路由

 

完整的代码可以见我的github:https://github.com/terry2870/hp-springboot

第一次写博客,如果有写的不好的,或者建议,意见都欢迎大家给我留言!

 

上一篇:平面中判断点在三角形内算法(重心法)


下一篇:MySQL数据备份与恢复