论持久战之Spring多数据源管理源码分析

Spring多数据源管理实现原理

[TOC]

应用场景:

大部分单一架构项目连接一台数据库服务器,但随着业务的增加数据库数据量不断飙升,数据库达到性能瓶颈,大部分技术人员都会对数据库主从配置;既然读写分离那就需要连接两个不同的数据库,这时候Spring多数据源管理类AbstractRoutingDataSource就要派上用场了(排除使用数据库集群管理工具统一管理的应用场景)

源码分析:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

    private Map<Object, Object> targetDataSources;

    private Object defaultTargetDataSource;

    private boolean lenientFallback = true;

    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();

    private Map<Object, DataSource> resolvedDataSources;

    private DataSource resolvedDefaultDataSource;
    ...
}

通过源码可以看出该类是一个抽象类,定义了6个属性。
targetDataSources:是一个map类型该属性正是用来维护项目中多个数据源
defaultTargetDataSource:通过属性名很直观的可以理解它的作用(默认数据源)
lenientFallback:默认为true,无需改动
dataSourceLookup:查找数据源接口的名称
resolvedDataSources:如果该字段没有赋值,就是targetDataSources
resolvedDefaultDataSource:改变后的数据源

public interface DataSourceLookup {

    /**
     * Retrieve the DataSource identified by the given name.
     * @param dataSourceName the name of the DataSource
     * @return the DataSource (never {@code null})
     * @throws DataSourceLookupFailureException if the lookup failed
     */
    DataSource getDataSource(String dataSourceName) throws DataSourceLookupFailureException;

}

该类是一个interface并且只有一个方法getDataSource,通过方法的参数名称应该清楚传入一个字符类型的数据源名称获取DataSource

深入理解:

使用数据源的目的就是要获取Connection,接下来就从AbstractRoutingDataSource的getConnection方法一探究竟。

@Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return determineTargetDataSource().getConnection(username, password);
    }

直接进入determineTargetDataSource方法

/**
     * Retrieve the current target DataSource. Determines the
     * {@link #determineCurrentLookupKey() current lookup key}, performs
     * a lookup in the {@link #setTargetDataSources targetDataSources} map,
     * falls back to the specified
     * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
     * @see #determineCurrentLookupKey()
     */
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        //该方法是一个抽象方法,返回要从resolvedDataSources查找key,该方法还会实现检查线程绑定事务上下文。
        Object lookupKey = determineCurrentLookupKey();
        //从resolvedDataSources中取出数据源并返回
        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;
    }

代码实现

实现AbstractRoutingDataSource重写determineCurrentLookupKey

/**
 * @author yangzhao
 * Created by 17/2/7.
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        String dataSourceName = DataSourceContextHolder.getDataSourceName();
        return dataSourceName;
    }
}

定义DataSourceContextHolder

/**
 * 该类内部维护了{@link ThreadLocal}
 * @author yangzhao
 * Created by 17/2/7.
 */
public class DataSourceContextHolder {

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    /**
     * @Description: 设置数据源类型
     * @param dataSourceName  数据源名称
     * @return void
     * @throws
     */
    public static void setDataSourceName(String dataSourceName) {contextHolder.set(dataSourceName);}

    /**
     * @Description: 获取数据源名称
     * @param
     * @return String
     * @throws
     */
    public static String getDataSourceName() {
        return contextHolder.get();
    }

    /**
     * @Description: 清除数据源名称
     * @param
     * @return void
     * @throws
     */
    public static void clearDataSource() {
        contextHolder.remove();
    }
}

通过ThreadLocal类使每个线程获取独立的数据源,防止并发访问时获取错误的数据源

基于SpringAop实现数据源动态切换

注解类DataSource
/**
 * 数据源
 * Created by yangzhao on 17/2/7.
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String value() default "defaultSource";
}
增强类(DataSouceAdvisor)
/**
 * 增强类
 * 实现MethodInterceptor接口,通过反射动态解析方法是否标注@DataSource {@link DataSource}注解。
 * 如果已标注@DataSource注解取值,set到{@link DataSourceContextHolder}
 * @author yangzhao
 *         create by 17/10/20
 */
@Component("dataSourceAdvisor")
public class DataSouceAdvisor implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        Method method = methodInvocation.getMethod();
        Object aThis = methodInvocation.getThis();
        //设置默认数据库
        DataSourceContextHolder.setDataSourceName("defaultSource");

        DataSource dataSource = aThis.getClass().getAnnotation(DataSource.class);
        if (dataSource!=null){
            DataSourceContextHolder.setDataSourceName(dataSource.value());
        }
        dataSource = method.getAnnotation(DataSource.class);
        if (dataSource!=null){
            DataSourceContextHolder.setDataSourceName(dataSource.value());
        }
        Object proceed = null;
        try {
            proceed = methodInvocation.proceed();
        }catch (Exception e){
            throw e;
        }finally {
            DataSourceContextHolder.clearDataSource();
        }
        return proceed;
    }
}

核心管理类(DataSourceManager真正实现切换)
/**
 * 数据源切换管理类
 *
 * @author yangzhao
 * Created by  17/2/7.
 */
@Component
public class DataSourceManager implements BeanFactoryPostProcessor {

    private final Logger logger = LogManager.getLogger(DataSourceManager.class);
    /**
     * 扫描包
     * 一般项目都是以com开头所以这里默认为com
     */
    private String pacakgePath = "com";

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        //getconfigs
        List<String> configs = getconfigs().stream().collect(Collectors.toList());

        //打印所有生成的expression配置信息
        configs.forEach(s -> logger.info(s));

        //设置aop信息
        setAopInfo(configs,beanFactory);
    }

    /**
     * 设置注册bean动态AOP信息
     * @param configs
     * @param beanFactory
     */
    private void setAopInfo(List<String> configs, ConfigurableListableBeanFactory beanFactory) {

        if (beanFactory instanceof BeanDefinitionRegistry){
            BeanDefinitionRegistry beanDefinitionRegistry = (BeanDefinitionRegistry) beanFactory;
            for (String config :configs) {
                //增强器
                RootBeanDefinition advisor = new RootBeanDefinition(DefaultBeanFactoryPointcutAdvisor.class);
                advisor.getPropertyValues().addPropertyValue("adviceBeanName",new RuntimeBeanReference("dataSourceAdvisor").getBeanName());
                //切点类
                RootBeanDefinition pointCut = new RootBeanDefinition(AspectJExpressionPointcut.class);
                pointCut.setScope(BeanDefinition.SCOPE_PROTOTYPE);
                pointCut.setSynthetic(true);
                pointCut.getPropertyValues().addPropertyValue("expression",config);

                advisor.getPropertyValues().addPropertyValue("pointcut",pointCut);
                //注册到spring容器
                String beanName = BeanDefinitionReaderUtils.generateBeanName(advisor, beanDefinitionRegistry,false);
                beanDefinitionRegistry.registerBeanDefinition(beanName,advisor);
            }
        }

    }
    public Set<String> getconfigs() {
        Set<String> configs = new HashSet<>();
        Reflections reflections = new Reflections(new ConfigurationBuilder().addUrls(ClasspathHelper.forPackage(pacakgePath)));
        //获取所有标记@DataSource的类
        Set<Class<?>> typesAnnotatedWith = reflections.getTypesAnnotatedWith(DataSource.class);
        Iterator<Class<?>> iterator = typesAnnotatedWith.iterator();
        while (iterator.hasNext()){
            Class<?> next = iterator.next();
            //获取该类所有方法
            Method[] declaredMethods = next.getDeclaredMethods();
            for (Method method:declaredMethods){
                String classAndMethod = method.getDeclaringClass().getCanonicalName()+"."+method.getName();
                //生成expression配置
                String expression = "execution (* "+classAndMethod+"(..))";
                configs.add(expression);
            }
        }
        reflections = new Reflections(new ConfigurationBuilder().setUrls(ClasspathHelper.forPackage(pacakgePath)).setScanners(new MethodAnnotationsScanner()));
        //获取所有类中标记@DataSource的方法
        Set<Method> methodsAnnotatedWith = reflections.getMethodsAnnotatedWith(DataSource.class);
        Iterator<Method> it = methodsAnnotatedWith.iterator();
        while (it.hasNext()){
            Method method = it.next();
            String classAndMethod = method.getDeclaringClass().getCanonicalName()+"."+method.getName();
            //生成expression配置
            String expression = "execution (* "+classAndMethod+"(..))";
            configs.add(expression);
        }
        return configs;
    }
}

项目地址:https://github.com/yz-java/multiple-data-sources

以上属于原创文章,转载请注明作者@怪咖
QQ:208275451

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354