SSM(八)动态切换数据源

作者: crossoverJie | 来源:发表于2017-01-05 01:46 被阅读1310次
    0

    前言

    在现在开发的过程中应该大多数朋友都有遇到过切换数据源的需求。比如现在常用的数据库读写分离,或者就是有两个数据库的情况,这些都需要用到切换数据源。

    手动切换数据源

    使用SpringAbstractRoutingDataSource类来进行拓展多数据源。

    该类就相当于一个dataSource的路由,用于根据key值来进行切换对应的dataSource

    下面简单来看下AbstractRoutingDataSource类的几段关键源码:

        @Override
        public Connection getConnection() throws SQLException {
            return determineTargetDataSource().getConnection();
        }
    
        @Override
        public Connection getConnection(String username, String password) throws SQLException {
            return determineTargetDataSource().getConnection(username, password);
        }
    
        /**
         * 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");
            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;
        }
    
        /**
         * Determine the current lookup key. This will typically be
         * implemented to check a thread-bound transaction context.
         * <p>Allows for arbitrary keys. The returned key needs
         * to match the stored lookup key type, as resolved by the
         * {@link #resolveSpecifiedLookupKey} method.
         */
        protected abstract Object determineCurrentLookupKey();
    

    可以看到其中获取链接的方法getConnection()调用的determineTargetDataSource则是关键方法。该方法用于返回我们使用的数据源。

    其中呢又是determineCurrentLookupKey()方法来返回当前数据源的key值。
    之后通过该key值在resolvedDataSources这个map中找到对应的value(该value就是数据源)。

    resolvedDataSources这个map则是在:

        @Override
        public void afterPropertiesSet() {
            if (this.targetDataSources == null) {
                throw new IllegalArgumentException("Property 'targetDataSources' is required");
            }
            this.resolvedDataSources = new HashMap<Object, DataSource>(this.targetDataSources.size());
            for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) {
                Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
                DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
                this.resolvedDataSources.put(lookupKey, dataSource);
            }
            if (this.defaultTargetDataSource != null) {
                this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
            }
        }
    

    这个方法通过targetDataSources这个map来进行赋值的。targetDataSources则是我们在配置文件中进行赋值的,下面会讲到。

    再来看看determineCurrentLookupKey()方法,从protected来修饰就可以看出是需要我们来进行重写的。

    DynamicDataSource 和 DataSourceHolder

    于是我新增了DynamicDataSource类,代码如下:

    package com.crossoverJie.util;
    
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    
    /**
     * Function:
     *
     * @author chenjiec
     *         Date: 2017/1/2 上午12:22
     * @since JDK 1.7
     */
    public class DynamicDataSource extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            return DataSourceHolder.getDataSources();
        }
    }
    

    代码很简单,继承了AbstractRoutingDataSource类并重写了其中的determineCurrentLookupKey()方法。

    这里直接用DataSourceHolder返回了一个数据源。

    DataSourceHolder代码如下:

    package com.crossoverJie.util;
    
    /**
     * Function:动态数据源
     *
     * @author chenjiec
     *         Date: 2017/1/2 上午12:19
     * @since JDK 1.7
     */
    public class DataSourceHolder {
        private static final ThreadLocal<String> dataSources = new ThreadLocal<String>();
    
        public static void setDataSources(String dataSource) {
            dataSources.set(dataSource);
        }
    
        public static String getDataSources() {
            return dataSources.get();
        }
    }
    

    这里我使用了ThreadLocal来保存了数据源,关于ThreadLocal的知识点可以查看以下这篇文章:
    解密ThreadLocal

    之后在Spring的配置文件中配置我们的数据源,就是上文讲到的为targetDataSources赋值

    <bean id="ssm1DataSource" class="com.alibaba.druid.pool.DruidDataSource"
              init-method="init" destroy-method="close">
            <!-- 指定连接数据库的驱动 -->
            <property name="driverClassName" value="${jdbc.driverClass}" />
            <property name="url" value="${jdbc.url}" />
            <property name="username" value="${jdbc.user}" />
            <property name="password" value="${jdbc.password}" />
            <!-- 配置初始化大小、最小、最大 -->
            <property name="initialSize" value="3" />
            <property name="minIdle" value="3" />
            <property name="maxActive" value="20" />
            <!-- 配置获取连接等待超时的时间 -->
            <property name="maxWait" value="60000" />
            <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
            <property name="timeBetweenEvictionRunsMillis" value="60000" />
            <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
            <property name="minEvictableIdleTimeMillis" value="300000" />
            <property name="validationQuery" value="SELECT 'x'" />
            <property name="testWhileIdle" value="true" />
            <property name="testOnBorrow" value="false" />
            <property name="testOnReturn" value="false" />
            <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
            <property name="poolPreparedStatements" value="true" />
            <property name="maxPoolPreparedStatementPerConnectionSize"
                      value="20" />
            <!-- 配置监控统计拦截的filters,去掉后监控界面sql无法统计 -->
            <property name="filters" value="stat" />
        </bean>
    
        <bean id="ssm2DataSource" class="com.alibaba.druid.pool.DruidDataSource"
              init-method="init" destroy-method="close">
            <!-- 指定连接数据库的驱动 -->
            <property name="driverClassName" value="${jdbc.driverClass}"/>
            <property name="url" value="${jdbc.url2}"/>
            <property name="username" value="${jdbc.user2}"/>
            <property name="password" value="${jdbc.password2}"/>
            <property name="initialSize" value="3"/>
            <property name="minIdle" value="3"/>
            <property name="maxActive" value="20"/>
            <!-- 配置获取连接等待超时的时间 -->
            <property name="maxWait" value="60000"/>
            <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
            <property name="timeBetweenEvictionRunsMillis" value="60000"/>
            <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
            <property name="minEvictableIdleTimeMillis" value="300000"/>
            <property name="validationQuery" value="SELECT 'x'"/>
            <property name="testWhileIdle" value="true"/>
            <property name="testOnBorrow" value="false"/>
            <property name="testOnReturn" value="false"/>
            <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
            <property name="poolPreparedStatements" value="true"/>
            <property name="maxPoolPreparedStatementPerConnectionSize"
                      value="20"/>
            <!-- 配置监控统计拦截的filters,去掉后监控界面sql无法统计 -->
            <property name="filters" value="stat"/>
        </bean>
        <bean id="dataSource" class="com.crossoverJie.util.DynamicDataSource">
            <property name="targetDataSources">
                <map key-type="java.lang.String">
                    <entry key="ssm1DataSource" value-ref="ssm1DataSource"/>
                    <entry key="ssm2DataSource" value-ref="ssm2DataSource"/>
                </map>
            </property>
            <!--默认数据源-->
            <property name="defaultTargetDataSource" ref="ssm1DataSource"/>
        </bean>
    

    这里分别配置了两个数据源:ssm1DataSourcessm2DataSource
    之后再通过Spring的依赖注入方式将两个数据源设置进targetDataSources

    接下来的用法相比大家也应该猜到了。

    就是在每次调用数据库之前我们都要先通过DataSourceHolder来设置当前的数据源。看下demo:

        @Test
        public void selectByPrimaryKey() throws Exception {
            DataSourceHolder.setDataSources(Constants.DATASOURCE_TWO);
            Datasource datasource = dataSourceService.selectByPrimaryKey(7);
            System.out.println(JSON.toJSONString(datasource));
        }
    

    详见我的单测。

    使用起来也是非常简单。但是不知道大家注意到没有,这样的做法槽点很多:

    1. 每次使用需要手动切换,总有一些人会忘记写(比如我)。
    2. 如果是后期需求变了,查询其他的表了还得一个个改回来。

    那有没有什么方法可以自动的帮我们切换呢?

    肯定是有的,大家应该也想得到。就是利用SpringAOP了。

    自动切换数据源

    首先要定义好我们的切面类DataSourceExchange:

    package com.crossoverJie.util;
    
    import org.aspectj.lang.JoinPoint;
    
    /**
     * Function:拦截器方法
     *
     * @author chenjiec
     *         Date: 2017/1/3 上午12:34
     * @since JDK 1.7
     */
    public class DataSourceExchange {
        
        /**
         *
         * @param point
         */
        public void before(JoinPoint point) {
    
            //获取目标对象的类类型
            Class<?> aClass = point.getTarget().getClass();
    
            //获取包名用于区分不同数据源
            String whichDataSource = aClass.getName().substring(25, aClass.getName().lastIndexOf("."));
            if ("ssmone".equals(whichDataSource)) {
                DataSourceHolder.setDataSources(Constants.DATASOURCE_ONE);
            } else {
                DataSourceHolder.setDataSources(Constants.DATASOURCE_TWO);
            }
    
        }
    
    
        /**
         * 执行后将数据源置为空
         */
        public void after() {
            DataSourceHolder.setDataSources(null);
        }
    
    }
    
    

    逻辑也比较简单,就是在执行数据库操作之前做一个切面。

    • 通过JoinPoint对象获取目标对象。
    • 在目标对象中获取包名来区分不同的数据源。
    • 根据不同数据源来进行赋值。
    • 执行完毕之后将数据源清空。

    关于一些JoinPoint的API:

    package org.aspectj.lang;
    import org.aspectj.lang.reflect.SourceLocation;
    public interface JoinPoint {
        String toString();         //连接点所在位置的相关信息
        String toShortString();     //连接点所在位置的简短相关信息
        String toLongString();     //连接点所在位置的全部相关信息
        Object getThis();         //返回AOP代理对象
        Object getTarget();       //返回目标对象
        Object[] getArgs();       //返回被通知方法参数列表
        Signature getSignature();  //返回当前连接点签名
        SourceLocation getSourceLocation();//返回连接点方法所在类文件中的位置
        String getKind();        //连接点类型
        StaticPart getStaticPart(); //返回连接点静态部分
    }
    

    为了通过包名来区分不同数据源,我将目录结构稍微调整了下:

    2

    将两个不同的数据源的实现类放到不同的包中,这样今后如果还需要新增其他数据源也可以灵活的切换。

    看下Spring的配置:

        <bean id="dataSourceExchange" class="com.crossoverJie.util.DataSourceExchange"/>
        <!--配置切面拦截方法 -->
        <aop:config proxy-target-class="false">
            <!--将com.crossoverJie.service包下的所有select开头的方法加入拦截
            去掉select则加入所有方法
            -->
            <aop:pointcut id="controllerMethodPointcut" expression="
            execution(* com.crossoverJie.service.*.select*(..))"/>
    
            <aop:pointcut id="selectMethodPointcut" expression="
            execution(* com.crossoverJie.dao..*Mapper.select*(..))"/>
    
            <aop:advisor advice-ref="methodCacheInterceptor" pointcut-ref="controllerMethodPointcut"/>
    
            <!--所有数据库操作的方法加入切面-->
            <aop:aspect ref="dataSourceExchange">
                <aop:pointcut id="dataSourcePointcut" expression="execution(* com.crossoverJie.service.*.*(..))"/>
                <aop:before pointcut-ref="dataSourcePointcut" method="before"/>
                <aop:after pointcut-ref="dataSourcePointcut" method="after"/>
            </aop:aspect>
        </aop:config>
    

    这是在我们上一篇整合redis缓存的基础上进行修改的。
    这样缓存和多数据源都满足了。

    实际使用:

        @Test
        public void selectByPrimaryKey() throws Exception {
            Rediscontent rediscontent = rediscontentService.selectByPrimaryKey(30);
            System.out.println(JSON.toJSONString(rediscontent));
        }
    
    3

    这样看起来就和使用一个数据源这样简单,再也不用关心切换的问题了。

    总结

    不过按照这样的写法是无法做到在一个事务里控制两个数据源的。这个我还在学习中,有相关经验的大牛不妨指点一下。

    项目地址:https://github.com/crossoverJie/SSM.git

    个人博客地址:http://crossoverjie.top

    GitHub地址:https://github.com/crossoverJie

    相关文章

      网友评论

      • aaa93ef869ac:为什么不直接定义两个使用不同数据源mapper呢,更简单直接
        crossoverJie:@Creair_6232 风险很高 之前我们就是手动切换 要用哪个数据源就用那个mapper 但是人多了之后总会有人记不住或者粗心。
      • c3747309e199:你的在有事务的情况下能切换吗
        crossoverJie:@西伯利亚虎 事务只针对于同一个库的操作啊 如果涉及到多库的话就是rpc调用了 这样就涉及到分布式事务了
        c3747309e199: @crossoverJie 那你们有事务的情况怎么处理,整个系统不可能一个事务也没用过吧
        crossoverJie:@西伯利亚虎 一个事务只能保证一个库。
      • 肉嘟嘟的小丸子:为啥按照您的写,不起总用奥,数据库没有切换过来
        肉嘟嘟的小丸子:@crossoverJie 或者您把您的qq说一下,加一下您,我的qq是191268132
        肉嘟嘟的小丸子:@crossoverJie 就是表不存在,感觉数据源没有切换不过来,这个是报错的日志,您能加一下qq吗191268132
        crossoverJie:有报错日志嘛?
      • 肉嘟嘟的小丸子:您的项目地址打不开,亲
        crossoverJie:这个地址:https://github.com/crossoverJie/SSM ? 可以打开呀

      本文标题:SSM(八)动态切换数据源

      本文链接:https://www.haomeiwen.com/subject/pdwavttx.html