Spring实现多数据源配置以及切换,并解决加入事务控制出现切换失败的问题

1.多数据源的配置:

    应用场景:(1)数据作读写分离,配置读库和写库。(2) 同步其他数据源数据库数据到项目默认的数据库对应表中,例如通过定时任务同步更新和修改操作

这里主要介绍的是通过Spring AOP,加上注解的方式,在进行Service方法的访问前,先通过前置通知Before,执行数据源切换操作,然后再执行dao层代码,进行数据库的相关操作。

 一.xml相关配置操作:

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xmlns:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:util="http://www.springframework.org/schema/util" xmlns:task="http://www.springframework.org/schema/task"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
      http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
      http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.1.xsd
      http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.1.xsd
      http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd
      http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.1.xsd
      http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.1.xsd
        http://www.springframework.org/schema/aop   http://www.springframework.org/schema/aop/spring-aop-4.1.xsd"
       default-lazy-init="true">  一开始没有添加此schema,导致在使用aop标签之后,启动项目报错

spring在启动的时候会验证 xml文档,这些引入的schema即用来验证配置文件的xml文档语法的正确性。

<!-- 使用Annotation自动注册Bean,解决事物失效问题:在主容器中不扫描@Controller注解,在SpringMvc中只扫描@Controller注解。  -->
<context:component-scan base-package="com.carzone"><!-- base-package 如果多个,用“,”分隔 -->
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- 定义事务 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
<!--&lt;!&ndash; 激活自动代理功能 &ndash;&gt; 因为spring aop就是通过代理的方式来实现的--> 
<aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/>
<!-- 定义数据源切面,在此类中通过注解的方式写入具体切面,以及在此切面要执行的数据源切换的操作 -->
<bean id="dataSourceAspect" class="com.carzone.common.multidatasource.DataSourceAspect" />
<!-- 配置 Annotation 驱动,扫描@Transactional注解的类定义事务  -->
<tx:annotation-driven transaction-manager="transactionManager" mode="proxy"/>
<!-- MyBatis end -->
<!-- 数据源配置, 使用 BoneCP 数据库连接池 -->
<bean id="dataSource1" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <!-- 数据源驱动类可不写,Druid默认会自动根据URL识别DriverClass -->
    <property name="driverClassName" value="${jdbc.driver}"/>

    <!-- 基本属性 url、user、password -->
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>

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

    <!-- 配置获取连接等待超时的时间 -->
    <property name="maxWait" value="60000"/>

    <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
    <property name="timeBetweenEvictionRunsMillis" value="60000"/>

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

    <property name="validationQuery" value="${jdbc.testSql}"/>
    <property name="testWhileIdle" value="true"/>
    <property name="testOnBorrow" value="false"/>
    <property name="testOnReturn" value="false"/>

    <!-- 打开PSCache,并且指定每个连接上PSCache的大小(Oracle使用)
    <property name="poolPreparedStatements" value="true" />
    <property name="maxPoolPreparedStatementPerConnectionSize" value="20" /> -->

    <!-- 配置监控统计拦截的filters -->
    <property name="filters" value="stat"/>
</bean>

<bean id="dataSource2" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <!-- 数据源驱动类可不写,Druid默认会自动根据URL识别DriverClass -->
    <property name="driverClassName" value="${jdbc.driver1}"/>

    <!-- 基本属性 url、user、password -->
    <property name="url" value="${jdbc.url1}"/>
    <property name="username" value="${jdbc.username1}"/>
    <property name="password" value="${jdbc.password1}"/>

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

    <!-- 配置获取连接等待超时的时间 -->
    <property name="maxWait" value="60000"/>

    <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
    <property name="timeBetweenEvictionRunsMillis" value="60000"/>

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

    <property name="validationQuery" value="${jdbc.testSql}"/>
    <property name="testWhileIdle" value="true"/>
    <property name="testOnBorrow" value="false"/>
    <property name="testOnReturn" value="false"/>

    <!-- 配置监控统计拦截的filters -->
    <property name="filters" value="stat"/>
</bean>

<!-- 确定所需要的数据连接 -->
<bean id="dataSource" class="com.carzone.common.multidatasource.ChooseDataSource">
    <property name="targetDataSources">
        <map key-type="java.lang.String">
            <entry value-ref="dataSource1" key="base"></entry>
            <entry value-ref="dataSource2" key="erp"></entry>
        </map>
    </property>
    <property name="defaultTargetDataSource" ref="dataSource1">
    </property>
</bean>

上面为数据源配置相关操作,我是命名为dataSource1 和dataSource2,同时默认库选中dataSource1,数据库连接的具体信息,我是写在properties文件中了,所以这里直接取值,这样以方便修改

上面是相应的xml配置,我只是列出了项目中部分的多数据源的配置。

下面是对应的所数据源的代码:

1、这是对应的确定数据源类,初始化后,dataSource被赋予相应的值,此处可以直接获得其key,进而确定相应的数据库连接

/** * 
 *确定数据源
 * @author   lb
 * */
public class ChooseDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return HandleDataSource.getDataSource();
    }
}
/**
     * 用注解来反射读取注解中输入的数据源已变确定要操作的数据源
     * @author lb
     *
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface DataSource {

        String value();
}
/**
 * 定义切面,以及前置通知和后置通知
 * @author lb
 */
@Aspect    // for aop
@Component // for auto scan
@Order(0)
public class DataSourceAspect {

    @Pointcut("execution( * com.carzone.modules.erp.service..*.*(..))")
    public void pointCut(){}


    /**
     * 在事务开始之前插入到service上,通过反射获取注解上的数据库
     * @param point
     */
    @Before(value="pointCut()")
    public void before(JoinPoint point){
//        //拦截的实体类
        Object target = point.getTarget();
        //拦截的方法名称
        String methodName = point.getSignature().getName();
        Class<?>[] classes = target.getClass().getInterfaces();
        //拦截的放参数类型
        Class<?>[] parameterTypes = ((MethodSignature) point.getSignature())
                .getMethod().getParameterTypes();
        try {
            Method m = classes[0].getMethod(methodName, parameterTypes);
            if (m != null && m.isAnnotationPresent(DataSource.class)) {
                DataSource data = m.getAnnotation(DataSource.class);
                HandleDataSource.setDataSource(data.value());
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }
    @After(value="pointCut()")
    public void after() {
        HandleDataSource.clearDataSource();//调用后清除数据源,系统然后就调用默认数据源了
    }
}
/**
 * 数据源get/set方法
 * @author  lb
 */
public class HandleDataSource {
    public static final ThreadLocal<String> holder = new ThreadLocal<String>();

    public static void setDataSource(String dataSource){
        holder.set(dataSource);
    }

    public static String getDataSource(){
        return holder.get();
    }
    public static void clearDataSource(){holder.remove();}
}
//接口中在需要切换数据源的方法上加DataSource注解,里面值为对应的key值
public interface SyncErpDataService {

@DataSource("erp")
List<ErpUserRole> findRelationList(String redisKey);

}

//实现层直接调用dao方法,实现数据的访问。

public List<ErpUserRole>  findRelationList(String redisKey){
     dao.findRelationList(redisKey);
}

 

问题:

 进行数据源切换配置运行成功后,我们数据源切换的Service层加入事务控制,发现此时数据源切换失败,dao只会访问默认的数据源。出现这个问题的原因,我在网上找了一下:

1. AOP可以触发数据源字符串的切换,这个没问题
2. 数据源真正切换的关键是 AbstractRoutingDataSource 的 determineCurrentLookupKey() **被调用,此方法是在open connection**时触发
3. 事务是在connection层面管理的,启用事务后,一个事务内部的connection是复用的,所以就算AOP切了数据源字符串,但是数据源并不会被真正修改

也就是说将数据源切换和事务处理都放在Service层,则数据源切换时失效的。 

下面提供两个解决方案:

1,在dao实现事务控制:此种方式不太合理,在于再dao层加入事务控制,无法保证一个方法内的事务的一致性

2.将数据源切换和事务开启按顺序进行,先切换,再开启事务。如下所示:

@DataSource("erp")
List<ErpUserRole> findRelationList(String redisKey);
List<ErpUserRole> findRelationList1(String redisKey);

@Override
public List<ErpUserRole>  findRelationList(String redisKey){
    return ((SyncErpDataService)AopContext.currentProxy()).findRelationList1(redisKey);
}

@Transactional(readOnly = true)
@Override
public List<ErpUserRole>  findRelationList1(String redisKey){
    return erpUserRoleDao.findRelationList(redisKey);
}

先进行数据源切换,再通过代理的方式调用另一个方法,该方法上开启事务,访问dao层。  本人测试不通过代理的方式进行方法调用的话,仍然没法数据源切换成功!!! 到此,文章结束~

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值