zoukankan      html  css  js  c++  java
  • dubbo服务+Spring事务+AOP动态数据源切换 出错

    1:问题描述,以及分析

            项目用了spring数据源动态切换,服务用的是dubbo。在运行一段时间后程序异常,更新操作没有切换到主库上。

    这个问题在先调用读操作后再调用写操作会出现

    经日志分析原因:

    第一:当程序运行一段时间后调用duboo服务时..([DubboServerHandler-192.168.1.106:20880-thread-199] [DubboServerHandler-192.168.1.106:20880-thread-200]) dubbo服务默认最大200线程(超过200个线程以后服务不会创建新的线程了),读操作与写操作有可能会在一个线程里(读操作的事务propagation是supports,写是required),当这种情况出现时MethodBeforeAdvice.before先执行,DataSourceSwitcher.setSlave()被调用,然后DynamicDataSource.determineCurrentLookupKey(此方法调用contextHolder.get获取数据源的key)被调用,此时数据源指向从库也就是只读库。当读操作执行完成后,dubbo在同一个线程(thead-200)里执行更新的操作(比如以update,insert开头的服务方法),这时会先执行DynamicDataSource.determineCurrentLookupKey,指向的是读库,然后执行MethodBeforeAdvice.before,DataSourceSwitcher.setMaster()被调用,注意,这时DynamicDataSource.determineCurrentLookupKey不会被再次调用,所以这时数据源仍然指向读库,异常发生了。(写从库了)

          DynamicDataSource.determineCurrentLookupKey 与DataSourceSwitcher.setXXX()方法的执行顺序是导致问题的关键,这个跟事务的advice与动态设置数据源的advice执行顺序有关.

    2:application.xml配置

    <bean id="parentDataSource" class="org.apache.commons.dbcp2.BasicDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="initialSize" value="20"/>
    <property name="maxTotal" value="50"/>
    <property name="maxIdle" value="10"/>
    <property name="testOnBorrow" value="true"/>
    <property name="testWhileIdle" value="true"/>
    <property name="testOnReturn" value="true"/>
    <property name="defaultAutoCommit" value="false"/>
    </bean>
    <!-- 主数据源-->
    <bean id="masterDataSource" parent="parentDataSource">
    <property name="url" value="jdbc:mysql://192.168.60.45:13306/ac_vote?autoReconnect=true&amp;useSSL=false"/>
    <property name="username" value="data"/>
    <property name="password" value="acfundata"/>
    </bean>
    <!-- 从数据源-->
    <bean id="slaveDataSource" parent="parentDataSource">
    <property name="url" value="jdbc:mysql://192.168.60.45:23306/ac_vote?autoReconnect=true&amp;useSSL=false"/>
    <property name="username" value="data"/>
    <property name="password" value="acfundata"/>
    </bean>
    <!-- 配置自定义动态数据源-->
    <bean id="dataSource" class="tv.acfun.service.common.database.DynamicDataSource">
    <property name="targetDataSources">
    <map key-type="java.lang.String">
    <entry key="slave" value-ref="slaveDataSource" />
    <entry key="master" value-ref="masterDataSource" />
    </map>
    </property>
    <property name="defaultTargetDataSource" ref="masterDataSource" />
    </bean>

    <!--开启自动代理功能 true使用CGLIB -->
    <aop:aspectj-autoproxy proxy-target-class="true"/>
    <!-- 声明AOP 切换数据源通知 类中加@Component 自动扫描xml中不用配<bean>了
    <bean id="dataSourceAdvice" class="tv.acfun.service.vote.aop.DataSourceAdvice" />
    -->
    <!-- 配置通知和切点 注意这个一定要配置在事务声明(txAdvice)之前 否则就会出现数据源切换出错  -->
    <aop:config>
        <aop:advisor pointcut="execution(* tv.acfun.service.vote.manager.impl.*ManagerImpl.*(..))" advice-ref="dataSourceAdvice" />
    </aop:config>

    <!-- 配置事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
    </bean>
    方式①
    <!--开启注解式事务扫描 要开启事务的service实现类中 加上@Transactional注解-->
    <tx:annotation-driven/>

    方式②(注释中) <!--未开启事务扫描时 需指定aop配置 声明那些类的哪些方法参与事务--start
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
    <tx:method name="add*" propagation="REQUIRED" />
    <tx:method name="create*" propagation="REQUIRED" />
    <tx:method name="save*" propagation="REQUIRED"/>
    <tx:method name="edit*" propagation="REQUIRED" />
    <tx:method name="update*" propagation="REQUIRED" />
    <tx:method name="delete*" propagation="REQUIRED" />
    <tx:method name="remove*" propagation="REQUIRED" />
    <tx:method name="find*" propagation="REQUIRED" read-only="true" />
    <tx:method name="query*" propagation="SUPPORTS" read-only="true" />
    <tx:method name="get*" propagation="SUPPORTS" read-only="true" />
    <!-- 对其它方法进行只读事务 -->
    <!--<tx:method name="*" propagation="SUPPORTS" read-only="true" />-->
    </tx:attributes>
    </tx:advice>
    <aop:config>
    <aop:advisor
    pointcut="execution(* tv.acfun.service.vote.manager..*Service.*(..))"
    advice-ref="txAdvice" />
    <aop:advisor
    pointcut="execution(* tv.acfun.service.vote.manager..*ServiceImpl.*(..))"
    advice-ref="txAdvice" />
    </aop:config>
    方式② end     -->

    3. DataSourceAdvice类


    @Slf4j
    @Aspect
    @Component
    public class DataSourceAdvice implements MethodBeforeAdvice, AfterReturningAdvice, ThrowsAdvice {

    // service方法执行之前被调用
    public void before(Method method, Object[] args, Object target) throws Throwable {
    log.info("切入点: " + target.getClass().getName() + "类中" + method.getName() + "方法");
    if (method.getName().startsWith("insert") || method.getName().startsWith("create")
    || method.getName().startsWith("save") || method.getName().startsWith("edit")
    || method.getName().startsWith("update") || method.getName().startsWith("delete")
    || method.getName().startsWith("remove")) {
    log.info("切换到: master");
    DataSourceSwitcher.setMaster();
    } else {
    log.info("切换到: slave");
    DataSourceSwitcher.setSlave();
    }
    }

    // service方法执行完之后被调用
    public void afterReturning(Object var1, Method var2, Object[] var3, Object var4) throws Throwable {
    DataSourceSwitcher.setMaster(); // ***** 加上这句解决运行数据库切换问题
    }

    // 抛出Exception之后被调用
    public void afterThrowing(Method method, Object[] args, Object target, Exception ex) throws Throwable {
    DataSourceSwitcher.setSlave();
    log.info("出现异常,切换到: slave");
    } 

    4. DataSourceSwitcher 类

    public class DataSourceSwitcher {

    private static final ThreadLocal contextHolder = new ThreadLocal();

    private static final String DATA_SOURCE_SLAVE = "slave" ;

    public static void setDataSource(String dataSource) {
    Assert.notNull(dataSource, "dataSource cannot be null");
    contextHolder.set(dataSource);
    }

    public static void setMaster(){
    clearDataSource();
    }

    public static void setSlave() {
    setDataSource( DATA_SOURCE_SLAVE);
    }

    public static String getDataSource() {
    return (String) contextHolder.get();
    }

    public static void clearDataSource() {
    contextHolder.remove();
    }
    } 

    5. DynamicDataSource 类

    public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
    return DataSourceSwitcher.getDataSource();
    }

    }

    和 http://my.oschina.net/mrXhuangyang/blog/500743 这个遇到一样问题


    或者基于注解@Aspect切面

    <?xml version="1.0" encoding="UTF-8"?>
    <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:tx="http://www.springframework.org/schema/tx"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"
    default-autowire="byName">
    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations">
    <list>
    <value>classpath:system.properties</value>
    </list>
    </property>
    </bean>
    <context:component-scan base-package="com.xxx.service.main"/>

    <!-- config mybatis -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="configLocation" value="classpath:/mybatis/mybatis-config.xml"/>
    <property name="typeAliasesPackage" value="com.xxx.service.entity"/>
    <property name="mapperLocations" value="classpath*:/mybatis/*Mapper.xml"/>
    </bean>
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.xxx.service.main.dao"/>
    <property name="annotationClass" value="com.xxx.service.main.dao.MyBatisRepository"/>
    </bean>

    <bean id="parentDataSource" class="org.apache.commons.dbcp2.BasicDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="initialSize" value="20"/>
    <property name="maxTotal" value="50"/>
    <property name="maxIdle" value="10"/>
    <property name="testOnBorrow" value="true"/>
    <property name="testWhileIdle" value="true"/>
    <property name="testOnReturn" value="true"/>
    <property name="defaultAutoCommit" value="false"/>
    </bean>

    <bean id="masterDataSource" parent="parentDataSource">
    <property name="url" value="jdbc:mysql://mysql.xxx.com:3306/system32?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;transformedBitIsBoolean=true"/>
    <property name="username" value="cache"/>
    <property name="password" value="test2"/>
    </bean>
    <!-- 从数据源-->
    <bean id="slaveDataSource" parent="parentDataSource">
    <property name="url" value="jdbc:mysql://mysql-slave.xxx.com:3306/system32?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;transformedBitIsBoolean=true"/>
    <property name="username" value="cache"/>
    <property name="password" value="test2"/>
    </bean>

    <bean id="dataSource" class="com.xxx.service.common.database.DynamicDataSource">
    <property name="targetDataSources">
    <map key-type="java.lang.String">
    <entry key="slave" value-ref="slaveDataSource" />
    </map>
    </property>
    <property name="defaultTargetDataSource" ref="masterDataSource" />
    </bean>

    <!--开启@Aspect注解支持--> <aop:aspectj-autoproxy/>

    <!--定义事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
    </bean> <!-- annotation-driven就是支持事务注解的(@Transaction) -->
    <tx:annotation-driven transaction-manager="transactionManager"/>

    <!-- config redis -->
    <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
    <property name="maxIdle" value="${spring.redis.pool.max-idle}" />
    <property name="maxTotal" value="${spring.redis.pool.max-active}" />
    <property name="minIdle" value="${spring.redis.pool.min-idle}" />
    <property name="maxWaitMillis" value="${spring.redis.pool.max-wait}" />
    </bean>

    <bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
    <property name="hostName" value="${spring.redis.host}" />
    <property name="port" value="${spring.redis.port}" />
    <property name="poolConfig" ref="poolConfig" />
    </bean>

    <bean id="stringRedisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
    <property name="connectionFactory" ref="connectionFactory" />
    </bean>
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
    <property name="connectionFactory" ref="connectionFactory" />
    <!-- key的序列化配置,不配置的话key值会有乱码 -->
    <property name="keySerializer">
    <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
    </property>
    <property name="hashKeySerializer">
    <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
    </property>
    </bean>

    </beans>


    @Aspect切面类
    package tv.acfun.service.main.aop;

    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.aop.AfterReturningAdvice;
    import org.springframework.aop.MethodBeforeAdvice;
    import org.springframework.aop.ThrowsAdvice;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;
    import tv.acfun.service.common.database.DataSourceSwitcher;

    import java.lang.reflect.Method;

    /**
    * @author xxx
    * @date 2016-11-24
    * @what 读写分离切面类
    */
    @Slf4j
    @Component
    @Aspect
    public class DataSourceAspect {

    @Pointcut("execution(* com.xxx.service.main.manager.impl.*ManagerImpl.*(..))")
    private void aspectjMethod(){}

    // service方法执行之前被调用
    @Before(value = "aspectjMethod()")
    public void before(JoinPoint joinPoint) throws Throwable {
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    Method method = methodSignature.getMethod();
    log.debug("切入点: " + joinPoint.getTarget().getClass().getName() + "类中" + method.getName() + "方法");
    if(method.getName().startsWith("add")
    || method.getName().startsWith("insert")
    || method.getName().startsWith("create")
    || method.getName().startsWith("save")
    || method.getName().startsWith("edit")
    || method.getName().startsWith("update")
    || method.getName().startsWith("delete")
    || method.getName().startsWith("remove")){
    log.debug("切换到: master");
    DataSourceSwitcher.setMaster();
    }else {
    log.debug("切换到: slave");
    DataSourceSwitcher.setSlave();
    }
    }

    }

    AOP方式资料:http://www.360doc.com/content/12/0602/15/7656232_215420487.shtml

    另外看到一个文章讲 

    @Order(-1)
    DataSourceAdvice前面加上一个Order注解 可以保证 数据源切换通知 在 事务通知前执行.
  • 相关阅读:
    day08超市商品库存案例
    day07
    day06_03
    day06_02
    day06_01
    最简单的库存管理java案例
    Day05_homework
    方法的使用注意事项
    day05
    冒泡排序
  • 原文地址:https://www.cnblogs.com/xmanblue/p/5281912.html
Copyright © 2011-2022 走看看