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&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&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><aop:config>
<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: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&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&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&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&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>
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注解 可以保證 數據源切換通知 在 事務通知前執行.