spring 动态切换数据源 多数据库

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/laojiaqi/article/details/78964862

1.背景

  • 对于数据量在1千万,单个mysql数据库就可以支持,但是如果数据量大于这个数的时候,例如1亿,那么查询的性能就会很低。此时需要对数据库做水平切分,常见的做法是按照用户的账号进行hash,然后选择对应的数据库。
  • 水平切分图,数据落入不同的库中

2.实现

2.1示意图

先来看下大致示意图:

  • 图1是比较常见的情况,单个数据库
  • 图2展示了web应用和数据库之间的一个中间层,这个中间层去选择使用哪个数据库。

2.2数据库配置

  • 首先我们需要配置多个数据源,我是用xml进行配置的其他方法大同小异,就是多建立了几个bean。
	<bean id="parentDataSource" abstract="true"
		  class="org.apache.tomcat.jdbc.pool.DataSource"
		  destroy-method="close"
		  p:maxWait="10000"
		  p:removeAbandoned="true"
		  p:removeAbandonedTimeout="180"
		  p:connectionProperties="clientEncoding=UTF-8"
		  p:validationQuery="SELECT 1"
		  p:validationInterval="30000"
		  p:testOnBorrow="false"
		  p:testOnReturn="false"
		  p:testWhileIdle="true"
		  p:timeBetweenEvictionRunsMillis="10000"
		  p:minEvictableIdleTimeMillis="60000"
		  p:logAbandoned="false"
		  p:defaultAutoCommit="true" />

	<bean id="dataSource" parent="parentDataSource"
		  p:driverClassName="com.mysql.jdbc.Driver"
		  p:username="${jdbc.user}"
		  p:password="${jdbc.password}"
		  p:initialSize="20"
		  p:maxActive="200"
		  p:maxIdle="200"
		  p:minIdle="5"/>

	<bean id="childDataSource1" parent="dataSource">
		<property name="url" value="${jdbc.url1}" />
	</bean>

	<bean id="childDataSource2" parent="dataSource">
		<property name="url" value="${jdbc.url2}" />
	</bean>
	
  • 这里我建了两个数据源bean的id分别为childDataSource1,childDataSource2,为了方便起见,这里只有数据库的url不同,故都继承了dataSource。

2.3 java实现

先把定义的多个数据库bean放一放,先来看下spring中对动态选择数据源的支持。

  • 在spring中有一个抽象类AbstractRoutingDataSource类,通过这个类可以实现动态选择数据源。来看下这个类的成员变量
    private Map<Object, Object> targetDataSources;
    private Object defaultTargetDataSource;
    private Map<Object, DataSource> resolvedDataSources;
  • targetDataSources中保存了key和数据库连接的映射关系,defaultTargetDataSource表示默认的链接,resolvedDataSources这个数据结构是通过targetDataSources构建而来,存储的结构也是数据库标识和数据源的映射关系。
  • 下面需要继承AbstractRoutingDataSource类,实现我们自己的数据库选择逻辑DataSourceSwitcher类,先上代码:
public class DataSourceSwitcher extends AbstractRoutingDataSource{

    private static final Logger LOGGER = LoggerFactory.getLogger("INTERACTIVE_LOGGER");

    private static final ThreadLocal<String> dataSourceKey = new ThreadLocal<String>();


    public static void clearDataSourceType() {
        LOGGER.debug("thread:{},remove,dataSource:{}",Thread.currentThread().getName());
        dataSourceKey.remove();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        String s = dataSourceKey.get();
        LOGGER.debug("thread:{},determine,dataSource:{}",Thread.currentThread().getName(),s);
        return s;
    }

    public static void setDataSourceKey(String dataSource) {
        LOGGER.debug("thread:{},set,dataSource:{}",Thread.currentThread().getName(),dataSource);
        dataSourceKey.set(dataSource);
    }
}
  • 第5行,threadLocal的成员变量dataSource(由于不同的请求所需要的数据源可能不一样),用于存储数据源标识。
  • 第8行,清除数据源操作.
  • 第14行,该方法决定了需要使用哪个数据库,该方法是抽象方法,必须由我们实现,那么现在来看下这个方法是如何使用的
	protected DataSource determineTargetDataSource() {
		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
		Object lookupKey = determineCurrentLookupKey();//这里获取数据库标识
		DataSource dataSource = this.resolvedDataSources.get(lookupKey);//获得具体的数据源
		if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
			dataSource = this.resolvedDefaultDataSource;
		}
		if (dataSource == null) {
			throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
		}
		return dataSource;
	}

所以我们需要在determineCurrentLookupKey方法中返回数据库标识即可

  • 第20行,设置数据源方法。

2.4 数据库配置和DataSourceSwitcher类结合

  • 现在把我们之前的数据库配置和DataSourceSwitcher进行合并,我在数据库的xml配置上添加如下配置:
	<bean id="dataSourceSwitcher" class="com.netease.mail.activity.service.switcher.DataSourceSwitcher">
		<property name="targetDataSources">
			<map>
				<entry key="ds1" value-ref="childDataSource1"/>
				<entry key="ds2" value-ref="childDataSource2"/>
			</map>
		</property>
		<property name="defaultTargetDataSource" ref="childDataSource1"/>
	</bean>

	<bean id="transactionManager"
		  class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSourceSwitcher" />
	</bean>

	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="configLocation" value="classpath:mybatis/sql-map-config.xml" />
		<property name="mapperLocations" value="classpath:mybatis/mapper/*.xml"/>
		<property name="dataSource" ref="dataSourceSwitcher" />
	</bean>
  • 可以看到,我对targetDataSources进行了初始化,ds1对应了数据源childDataSource1;ds2对应了数据源childDataSource2。
  • 使用的话只要调用DataSourceSwitcher.setDataSourceKey(“ds1”),即将数据源切换到了childDataSource1

2.5 增加切面处理

  • 如果每次执行方法都要设置一下数据源实在是件很麻烦的事情,另外我们需要对某个key进行hash后选择数据库,这块也没有实现。现在借助spring切面的功能,可以解决这两个问题。
  • 大致思路如下:
2.5.1 自定义注解
  • 定义UseDataSource注解
/**
 * 数据源注解
 * Created by hzlaojiaqi on 2017/12/26.
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseDataSource {

     /**
      * 数据源
      * @return
      */
     DataSourceType value() default DataSourceType.SOURCE_1;


     /**
      * 是否使用hashkey,若为true,则使用对应字段的哈希值进行计算,选择数据源,
      * 且指定的{@link DataSourceType}不起作用
      * @return
      */
     boolean useHashKey() default false;

}

  • DataSourceType为枚举类型,如下
@Getter
public enum DataSourceType {
    SOURCE_1("ds1", "数据源1-默认数据源"),
    SOURCE_2("ds2", "数据源2");

    DataSourceType(String source, String desc) {
        this.source = source;
        this.desc = desc;
    }

    String source;

    String desc;

    /*
     * @param hashKey
     * @return
     */
    public static String getByKey(String hashKey){
        //根据hashkey来获取所需要的数据源
        int i = Math.abs(hashKey.hashCode()) % DataSourceType.values().length;
        return DataSourceType.values()[i].getSource();
    }

}

  • useHashKey是否使用hashkey。
  • 定义DSKey注解,该注解用于标注在对应的方法变量上,表示对该变量的值进行hash。
/**
 *
 * 数据源选择 注解
 * 用在参数上,表示使用对应字段的hashcode来选择数据库
 * Created by hzlaojiaqi on 2017/12/26.
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface DSKey {

    String value() default "";
}


2.5.2 aop进行拦截
  • 由于只需要对UseDataSource这个注解进行拦截,因此切点可以设置如下
   @Pointcut("@annotation(com.netease.mail.activity.aop.annotation.UseDataSource)")
    public void useDataSource() {
    } 
  • 流程图如下所示

  • 切点处理Around方法如下:

    /**
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("useDataSource() && @annotation(anno)")
    public Object dataSourceSwitcher(ProceedingJoinPoint joinPoint, UseDataSource anno) throws Throwable {
        String ds="";
        //若使用hashkey,则根据hashkey进行选择数据源
        if(anno.useHashKey()){
            ds=DataSourceType.getByKey(getHashKeyFromMethod(joinPoint));
        }else{
            //直接获取数据源
            DataSourceType value = anno.value();
            ds=value.getSource();
        }
        //设置数据源
        DataSourceSwitcher.setDataSourceKey(ds);
        try {
            //执行方法
            Object result = joinPoint.proceed();
            return result;
        }catch (Exception e){
            throw e;
        }finally {
            //切换回原来的数据源(重要)  
            DataSourceSwitcher.setDataSourceKey(DataSourceType.SOURCE_1.getSource());
        }
    }
  • getHashKeyFromMethod方法获取了用@DSKey标注的变量的值,实现如下:

    /**
     * @param joinPoint
     * @return
     */
    public String getHashKeyFromMethod(ProceedingJoinPoint joinPoint){
        MethodSignature signature=MethodSignature.class.cast(joinPoint.getSignature());
        Method method = signature.getMethod();
        Object[] args = joinPoint.getArgs();
        Parameter[] declaredFields = method.getParameters();
        int index=0;
        for(Parameter temp:declaredFields){
            Annotation[] annotations = temp.getAnnotations();
            for(Annotation anTemp:annotations){
                if(anTemp instanceof DSKey){
                    return String.valueOf(args[index]);
                }
            }
            index++;
        }
        throw new RuntimeException("can not get field with @DsKey annotation");
    }

3.使用

  • 我们看下实际的效果,定义两个controller, insert用的是固定的Dasrouce.SOURCE_1,insert2用的是uid的hash值进行数据源的选择。
    @RequestMapping(value = "/ajax/insert.do",method = RequestMethod.GET)
    @ResponseBody
    @UseDataSource(DataSourceType.SOURCE_1)
    public AjaxResult insert(@RequestParam String uid, HttpServletRequest httpServletRequest){
        WebCouponWinner webCouponWinner=new WebCouponWinner();
        webCouponWinner.setUid(uid);
        webCouponWinner.setInsertTime(TimeUtil.now());
        webCouponWinnerDao.insert(webCouponWinner);
        return new AjaxResult(RetCode.SUCCESS);
    }

    @RequestMapping(value = "/ajax/insert2.do",method = RequestMethod.GET)
    @ResponseBody
    @UseDataSource(useHashKey = true)
    public AjaxResult insert2(@RequestParam @DSKey String uid, HttpServletRequest httpServletRequest){
        WebCouponWinner webCouponWinner=new WebCouponWinner();
        webCouponWinner.setUid(uid);
        webCouponWinner.setInsertTime(TimeUtil.now());
        webCouponWinnerDao.insert(webCouponWinner);
        return new AjaxResult(RetCode.SUCCESS);
    }
  1. 执行/ajax/insert.do,uid分别传1和2,得到如下结果

    两条记录都在同一个数据库中

  2. 再执行/ajax/insert2.do,uid分别传1和2,得到如下结果

    以及

    两条记录在不同的库中,符合预期。

  3. 完整的DataSourceAsp,需要注意下该切面必须要在事务注解@Transactional之前,由于在开始事务之前就需要确定数据源,所以设置DataSourceAsp的**@Order(Ordered.LOWEST_PRECEDENCE-1),@Transactional的order是最小值**

package com.netease.mail.activity.aop;

import com.netease.mail.activity.aop.annotation.DSKey;
import com.netease.mail.activity.aop.annotation.UseDataSource;
import com.netease.mail.activity.aop.type.DataSourceType;
import com.netease.mail.activity.exception.custom.BizException;
import com.netease.mail.activity.service.complex.MonitorService;
import com.netease.mail.activity.service.switcher.DataSourceSwitcher;
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.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

/**
 *
 * 数据源切换
 * Created by hzlaojiaqi on 2017/12/26.
 */
@Component
@Aspect
@Slf4j(topic = "THIRDPARTY_LOGGER")
@Order(Ordered.LOWEST_PRECEDENCE-1)
public class DataSourceAsp {

    @Autowired
    MonitorService mMonitor;

    /**
     * 针对所有的Mapped
     */
    @Pointcut("@annotation(com.netease.mail.activity.aop.annotation.UseDataSource)")
    public void useDataSource() {
    }

    /**
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("useDataSource() && @annotation(anno)")
    public Object dataSourceSwitcher(ProceedingJoinPoint joinPoint, UseDataSource anno) throws Throwable {
        String ds="";
        if(anno.useHashKey()){
            ds=DataSourceType.getByKey(getHashKeyFromMethod(joinPoint));
        }else{
            DataSourceType value = anno.value();
            ds=value.getSource();
        }
        DataSourceSwitcher.setDataSourceKey(ds);
        try {
            Object result = joinPoint.proceed();
            return result;
        }catch (Exception e){
            throw e;
        }finally {
            DataSourceSwitcher.setDataSourceKey(DataSourceType.SOURCE_1.getSource());
        }
    }


    /**
     * @param joinPoint
     * @return
     */
    public String getHashKeyFromMethod(ProceedingJoinPoint joinPoint){
        MethodSignature signature=MethodSignature.class.cast(joinPoint.getSignature());
        Method method = signature.getMethod();
        Object[] args = joinPoint.getArgs();
        Parameter[] declaredFields = method.getParameters();
        int index=0;
        for(Parameter temp:declaredFields){
            Annotation[] annotations = temp.getAnnotations();
            for(Annotation anTemp:annotations){
                if(anTemp instanceof DSKey){
                    return String.valueOf(args[index]);
                }
            }
            index++;
        }
        throw new BizException("can not get field with @DsKey annotation");
    }


}

github地址

为了方便使用,我把这个类单独抽离出来类,github地址,注意下,还没有上传到maven库,目前要使用的话,把这些包都拷到自己项目中吧:)

猜你喜欢

转载自blog.csdn.net/laojiaqi/article/details/78964862