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>
<!--<!– 激活自动代理功能 –> 因为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层。 本人测试不通过代理的方式进行方法调用的话,仍然没法数据源切换成功!!! 到此,文章结束~