最近有个数据归集的需求用到了多数据源,在业务库保存后同时向归集库插入或数据。之前好像还没做过这块的东西,简单记录下防止下次又忘记了~
踩过的几个坑都是某些知识点不熟悉导致的,而且都是框架配置相关的..
先上代码,再扯淡
两个库都是mysql,不同数据库应该就是配置不一样,使用的druid数据库连接池
- #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}
- 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";
-
- }
- 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
key = new ThreadLocal(); -
- @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();
- }
-
- }
- 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());
- }
- }
- }
-
- <bean id="dataSourcePrimary" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
-
- <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}" />
-
- <property name="filters" value="${druid.filters}" />
- bean>
-
-
-
- <bean id="dataSourceSecend" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
-
- <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}" />
-
- <property name="filters" value="${druid.filters}" />
- bean>
-
-
-
- <bean id="dataSource" class="cn.pinming.datasource.DynamicDataSource">
- <property name="targetDataSources">
- <map key-type="java.lang.String">
-
- <entry key="primary" value-ref="dataSourcePrimary"/>
-
- <entry key="secend" value-ref="dataSourceSecend"/>
- map>
- property>
-
- <property name="defaultTargetDataSource" ref="dataSourcePrimary"/>
- bean>
-
-
- <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"/>
-
-
- <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>
-
-
- <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"/>
-
- <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也一样的,实际根据需求来就行。
1、SpringAOP切面类不运行的问题 ,注意配置写在spring-config.xml而不是spring-mvc.xml中。
2、aop:pointcut的配置说明
其中expression="execution(* com.aop.service..*(..))"的配置规则如下: execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?) 参数名(参数模式)稍微复杂一些: 问题:在controller层调用不同的service可以正常切换数据源,但是在service层调用另一个数据源就会失效。 原因是AbstractRoutingDataSource 加事务导致切换数据源失败。。 解决可参考:SpringBoot多数据源切换详解,以及开启事务后数据源切换失败处理_zzhongcy的博客-CSDN博客_动态数据源事务失效
execution(方法的操作权限 返回值类型模式 方法所在的包 方法名 (参数名) 异常)
() 匹配不带参数的方法,
(..) 匹配任何数量(零个或多个)参数。
(*) 模式匹配采用任何类型的一个参数的方法
(*,String) 匹配一个带有两个参数的方法。第一个可以是任何类型,而第二个必须是String
返回值,方法名,参数名,必须有,其他可选
执行任何公共方法:
execution(public * *(..))
执行名称以以下开头的任何方法set:
execution(* set*(..))
执行AccountService接口定义的任何方法:
execution(* com.xyz.service.AccountService.*(..))
执行service包中定义的任何方法:
execution(* com.xyz.service.*.*(..))
执行服务包或其子包中定义的任何方法:
execution(* com.xyz.service..*.*(..))