SSM框架使用多数据源(druid连接池)

 最近有个数据归集的需求用到了多数据源,在业务库保存后同时向归集库插入或数据。之前好像还没做过这块的东西,简单记录下防止下次又忘记了~

踩过的几个坑都是某些知识点不熟悉导致的,而且都是框架配置相关的..

先上代码,再扯淡

两个库都是mysql,不同数据库应该就是配置不一样,使用的druid数据库连接池

一、修改properties配置文件中的数据库信息

#jdbc configure

connection.url=${db.url}
connection.username=${db.username}
connection.password=${db.password}
connection.driver=${connection.driver_class}

#删掉此配置 退回单数据源
connection.url2=${tradding.db.url2}
connection.username2=${tradding.db.username2}
connection.password2=${tradding.db.password2}
connection.driver2=${connection.driver_class2}

二、创建@DataSourceAnnotation 注解使用于aop进行数据源的切换

package cn.xxx.datasource;

import java.lang.annotation.*;

/**
 * @author zj
 * @creatTime 2022-11-22
 * @description
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSourceAnnotation {

    String value();

    String primary = "primary";

    String secend= "secend";

}

三、创建动态数据源DynamicDataSource继承AbstractRoutingDataSource

package cn.xxx.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * @author zj
 * @creatTime 2022-11-18
 * @description 配置多个数据源
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * 数据源标识,保存在线程变量中,避免多线程操作数据源时互相干扰
     */
    private static final ThreadLocal<String> key = new ThreadLocal<String>();

    @Override
    protected Object determineCurrentLookupKey() {
        return key.get();
    }

    /**
     * 设置数据源
     *
     * @param dataSource 数据源名称
     */
    public static void setDataSource(String dataSource) {
        key.set(dataSource);
    }

    /**
     * 获取数据源
     *
     * @return
     */
    public static String getDatasource() {
        return key.get();
    }

    /**
     * 清除数据源
     */
    public static void clearDataSource() {
        key.remove();
    }

}

四、创建一个aop切面作用于目标方法上

package cn.xxx.datasource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @author zj
 * @creatTime 2022-11-22
 * @description
 */
@Component
public class DynamicDataSourceAspect implements MethodBeforeAdvice, AfterReturningAdvice {

    Logger log = LoggerFactory.getLogger(DynamicDataSourceAspect.class);


    @Override
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
        //这里做一个判断,有使用DataSourceAnnotation注解时才关闭数据源,有一个主要的数据源,就没有必要每次都去关闭
        if (method.isAnnotationPresent(DataSourceAnnotation.class)) {
            DynamicDataSource.clearDataSource();
            log.debug("数据源已关闭");
        }

    }

    /**
     * 拦截目标方法,获取由@DataSourceAnnotation指定的数据源标识,设置到线程存储中以便切换数据源
     */
    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        if (method.isAnnotationPresent(DataSourceAnnotation.class)) {
            DataSourceAnnotation dataSourceAnnotation = method.getAnnotation(DataSourceAnnotation.class);
            DynamicDataSource.setDataSource(dataSourceAnnotation.value());
            log.debug("数据源切换为:" + DynamicDataSource.getDatasource());
        }
    }
}

五、spring-config.xml配置多数据源

  <!-- 数据源1 -->
    <bean id="dataSourcePrimary" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!-- 基本属性 url、user、password -->
        <property name="url" value="${connection.url}"/>
        <property name="username" value="${connection.username}"/>
        <property name="password" value="${connection.password}"/>

        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="${druid.initialSize}"/>
        <property name="minIdle" value="${druid.minIdle}"/>
        <property name="maxActive" value="${druid.maxActive}"/>

        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="${druid.maxWait}"/>
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="${druid.timeBetweenEvictionRunsMillis}" />

        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="${druid.minEvictableIdleTimeMillis}" />

        <property name="validationQuery" value="${druid.validationQuery}" />
        <property name="testWhileIdle" value="${druid.testWhileIdle}" />
        <property name="testOnBorrow" value="${druid.testOnBorrow}" />
        <property name="testOnReturn" value="${druid.testOnReturn}" />
        <!-- 配置监控统计拦截的filters -->
        <property name="filters" value="${druid.filters}" />
    </bean>

    <!-- 数据源2 -->

    <bean id="dataSourceSecend" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!-- 基本属性 url、user、password -->
        <property name="url" value="${connection.url2}"/>
        <property name="username" value="${connection.username2}"/>
        <property name="password" value="${connection.password2}"/>

        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="${druid.initialSize}"/>
        <property name="minIdle" value="${druid.minIdle}"/>
        <property name="maxActive" value="${druid.maxActive}"/>

        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="${druid.maxWait}"/>
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="${druid.timeBetweenEvictionRunsMillis}" />

        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="${druid.minEvictableIdleTimeMillis}" />

        <property name="validationQuery" value="${druid.validationQuery}" />
        <property name="testWhileIdle" value="${druid.testWhileIdle}" />
        <property name="testOnBorrow" value="${druid.testOnBorrow}" />
        <property name="testOnReturn" value="${druid.testOnReturn}" />
        <!-- 配置监控统计拦截的filters -->
        <property name="filters" value="${druid.filters}" />
    </bean>


  <!-- 动态数据源配置  如果之前没有这块那这个id直接用原数据源的名称,然后把原来数据源重命名,这样有些配置就不会漏改。。-->
    <bean id="dataSource" class="cn.pinming.datasource.DynamicDataSource">
        <property name="targetDataSources">
            <map key-type="java.lang.String">
                <!-- 主库-1 -->
                <entry key="primary" value-ref="dataSourcePrimary"/>
                <!-- 副库-2 -->
                <entry key="secend" value-ref="dataSourceSecend"/>
            </map>
        </property>
        <!--默认数据源-->
        <property name="defaultTargetDataSource" ref="dataSourcePrimary"/>
    </bean>

六、配置事务和AOP

 <!--事务管理器配置-->

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

    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="doReweight" propagation="REQUIRES_NEW"/>
            <tx:method name="doClear*" propagation="REQUIRES_NEW"/>
            <tx:method name="doSend*" propagation="REQUIRES_NEW"/>
            <tx:method name="doBatchSave*" propagation="REQUIRES_NEW"/>
            <tx:method name="time*" propagation="REQUIRES_NEW"/>

            <!--hibernate4必须配置为开启事务 否则 getCurrentSession()获取不到-->
            <tx:method name="get*" propagation="REQUIRED" read-only="true"/>
            <tx:method name="count*" propagation="REQUIRED" read-only="true"/>
            <tx:method name="find*" propagation="REQUIRED" read-only="true"/>
            <tx:method name="search*" propagation="REQUIRED" read-only="true"/>
            <tx:method name="select*" propagation="REQUIRED" read-only="true"/>
            <tx:method name="package*" propagation="REQUIRED" read-only="true"/>
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>


    <bean id="dynamicDataSourceAspect" class="cn.pinming.datasource.DynamicDataSourceAspect">
    </bean>

    <!--设置切面  com.test.service.*.impl.*()-->
    <aop:config proxy-target-class="true">
        <aop:pointcut id="myPointcut" expression="execution(* cn.xxx.test.service.TestSourceService.*(..))"/>
        <aop:advisor advice-ref="dynamicDataSourceAspect" pointcut-ref="myPointcut" order="1"/>
        <!--把事务控制在Service层-->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="myPointcut" order="2"/>
    </aop:config>

这里需要注意的是order值越小,优先级越高,所以切换数据源order的值要比事务切面的值小,否则会出现数据源切换失败!

七、切换数据源,实际使用

在需要切换为非默认数据的方法上加@DataSourceAnnotation(DataSourceAnnotation.secend)就可以完成数据源的切换了。

controller层:

/**
 * @author zj
 * @creatTime 2022-11-18
 * @description
 */
@Controller
public class TestController {

    @Autowired
    private TPProjectMapper projectMapper;
    @Autowired
    private TestSourceService testSourceService;

    @RequestMapping("/ocx/saveOrUpdateTest")
    @ResponseBody
    public Result saveOrUpdateTest(){
        Result result = new Result();
        //先测试插入 监督库
        TPProject project = new TPProject();
        project.setProjectId(111221212L);
        project.setProjectName("test测试多数据源——监督库");
        ContextFacade.initEntity(project);
        projectMapper.insert(project);
        //再测试插入 交易库
        testSourceService.saveOrUpdateTest();
        //再测试修改 监督库
        TPProject project1 = projectMapper.selectByPrimaryKey(111221212L);
        project1.setProjectId(111221212L);
        project1.setProjectName("test测试多数据源——监督库__二次修改结果");
        projectMapper.updateByPrimaryKey(project1);

        return result;
    }


}

service层:

package cn.xxx.test.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author zj
 * @creatTime 2022-11-18
 * @description
 */
@Service
public class TestSourceService {

    @Autowired
    private TPProjectMapper tpProjectMapper;

    @DataSourceAnnotation(DataSourceAnnotation.secend)
    public Result saveOrUpdateTest(){
        Result result = new Result();
        TPProject project = new TPProject();
        project.setProjectId(111221213L);
        project.setProjectName("test测试多数据源——交易库");
        ContextFacade.initEntity(project);
        tpProjectMapper.insert(project);
        return result;
    }
}

八、结果

主库:

副库:

 这里为了方便用了同一个表,只是不同数据库,所以mapper和sql也一样的,实际根据需求来就行。

九、Other

1、SpringAOP切面类不运行的问题 ,注意配置写在spring-config.xml而不是spring-mvc.xml中。

2、aop:pointcut的配置说明

<aop:pointcut expression="execution(* com.aop.service..*(..))" 

其中expression="execution(* com.aop.service..*(..))"的配置规则如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
execution(方法的操作权限    返回值类型模式    方法所在的包        方法名 (参数名)  异常)

参数名(参数模式)稍微复杂一些:
()                 匹配不带参数的方法,
(..)               匹配任何数量(零个或多个)参数。
(*)                模式匹配采用任何类型的一个参数的方法
(*,String)     匹配一个带有两个参数的方法。第一个可以是任何类型,而第二个必须是String


返回值,方法名,参数名,必须有,其他可选


执行任何公共方法:
execution(public * *(..))


执行名称以以下开头的任何方法set:
execution(* set*(..))


执行AccountService接口定义的任何方法:
execution(* com.xyz.service.AccountService.*(..))


执行service包中定义的任何方法:
execution(* com.xyz.service.*.*(..))


执行服务包或其子包中定义的任何方法:
execution(* com.xyz.service..*.*(..))

问题:在controller层调用不同的service可以正常切换数据源,但是在service层调用另一个数据源就会失效。 原因是AbstractRoutingDataSource  加事务导致切换数据源失败。。 

解决可参考:SpringBoot多数据源切换详解,以及开启事务后数据源切换失败处理_zzhongcy的博客-CSDN博客_动态数据源事务失效

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值