前言
本来只是动态数据源切换的话,是不准备记录下来当博客发表的。但是在使用Spring + Hibernate实现数据源切换时,遇到了切换失败的问题。无论是查看源码还是debug调试都解决不了,最后还是在网上找到了答案,这里留一个悬念。
所以本文主旨在查阅大量资料后解决动态切换数据源失败的问题,顺便在前面介绍如何进行配置动态切换数据源。所有场景以Spring 4.2.6 + Hibernate 4.2.7 + proxool为标准,后文不再赘述。
正文
设计思路
Hibernate所有事务的操作都是和session绑定,而session由sessionFactory生成。sessionFactory作为一个重量级别的对象,不宜根据数据源的不同生成多个sessionFactory对象。这也是使用单例的原因。那么想动态切换数据源只能从sessionFactory依赖的dataSource动手。而Spring从2.0版本就提供了一个可以动态切换的数据源AbstractRoutingDataSource,那么所有的一切就有了解决方案:
设计实现
Spring关于数据源事务管理器的配置:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:p="http://www.springframework.org/schema/p" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <bean id="parentDataSource" class="org.logicalcobwebs.proxool.ProxoolDataSource"> <property name="user" value="${jdbc.username}"/> <property name="driver" value="${jdbc.driverClassName}"/> <property name="maximumConnectionCount" value="1000"/> <property name="minimumConnectionCount" value="20"/> <property name="houseKeepingSleepTime" value="300000"/><!-- 自动侦查各个连接状态的时间间隔,空闲的回收,超时的销毁,单位毫秒 --> <property name="simultaneousBuildThrottle" value="200"/> <property name="prototypeCount" value="5"/> <property name="maximumActiveTime" value="172800000"/> <property name="maximumConnectionLifetime" value="180000000"/> <property name="testBeforeUse" value="false"/> <property name="houseKeepingTestSql" value="select CURRENT_DATE"/> </bean> <bean parent="parentDataSource" id="production_env"> <property name="driverUrl" value="${jdbc.url.local}"/> <property name="password" value="${jdbc.password.local}"/> <property name="alias" value="proxool.a2"/> </bean> <bean parent="parentDataSource" id="test_env"> <property name="driverUrl" value="${jdbc.url.server.test}"/> <property name="password" value="${jdbc.password.server.test}"/> <property name="alias" value="proxool.a1"/> </bean> <bean class="com.config.datasource.DynamicDataSource" id="dataSource"> <property name="targetDataSources"> <map key-type="java.lang.String"> <entry value-ref="production_env" key="pro_db"></entry> <entry value-ref="test_env" key="test_db"></entry> </map> </property> <property name="defaultTargetDataSource" ref="production_env"></property> </bean> <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"> <property name="mappingResources"> <list> <!-- 配置所有PO映射文件 --> <value >**.**.hbm.xml</value> </list> </property> <property name="configLocation"> <value>classpath:hibernate.cfg.xml</value> </property> <property name="dataSource" ref="dataSource"> </property> </bean> <bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory"></property> </bean> <!-- annotation配置事务规则 --> <tx:annotation-driven transaction-manager="transactionManager" /> </beans>
数据源的确定
public class DynamicDataSource extends AbstractRoutingDataSource{ //private static final Logger log = Logger.getLogger(DynamicDataSource.class); @Override protected Object determineCurrentLookupKey() { String type = DataSourceHolder.getType(); //log.info("switch_ds:"+type); System.out.println("switch_ds:"+type); return type; } }
// 保留AbstractRoutingDataSource类中的关键实现方法 public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { private Map<Object, Object> targetDataSources; private Object defaultTargetDataSource; private boolean lenientFallback = true; private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); private Map<Object, DataSource> resolvedDataSources; private DataSource resolvedDefaultDataSource; // Spring初始化时的对属性的初始化,获取配置文件中配置的数据源 // 对配置的key名和配置的数据源Object以map形式保存。 @Override public void afterPropertiesSet() { if (this.targetDataSources == null) { throw new IllegalArgumentException("Property 'targetDataSources' is required"); } this.resolvedDataSources = new HashMap<Object, DataSource>(this.targetDataSources.size()); for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) { Object lookupKey = resolveSpecifiedLookupKey(entry.getKey()); DataSource dataSource = resolveSpecifiedDataSource(entry.getValue()); this.resolvedDataSources.put(lookupKey, dataSource); } if (this.defaultTargetDataSource != null) { this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource); } } protected Object resolveSpecifiedLookupKey(Object lookupKey) { return lookupKey; } protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException { if (dataSource instanceof DataSource) { return (DataSource) dataSource; } else if (dataSource instanceof String) { return this.dataSourceLookup.getDataSource((String) dataSource); } else { throw new IllegalArgumentException( "Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource); } } // dataSource获取连接的实现 @Override public Connection getConnection() throws SQLException { return determineTargetDataSource().getConnection(); } @Override public Connection getConnection(String username, String password) throws SQLException { return determineTargetDataSource().getConnection(username, password); } @Override @SuppressWarnings("unchecked") public <T> T unwrap(Class<T> iface) throws SQLException { if (iface.isInstance(this)) { return (T) this; } return determineTargetDataSource().unwrap(iface); } @Override public boolean isWrapperFor(Class<?> iface) throws SQLException { return (iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface)); } // 根据key获取配置中对应的数据源 protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = this.resolvedDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } return dataSource; } // 子类实现,获取对应的key protected abstract Object determineCurrentLookupKey(); }
动态切换的基础容器
public enum DataSourceType { PRODUCTION_DB("pro_db"),TEST_DB("test_db"); private String type; DataSourceType(String type) { this.type = type; } public String type() { return type; } }
// 实现这个容器以后,已经可以通过编程方式,在需要切换数据源处通过代码显示更改 // DataSourceHolder.setType();需要切到目标数据源后,使用setType()方法设置为该数据源的key即可 public class DataSourceHolder { private static ThreadLocal<String> dataSourceHolder = new ThreadLocal<>(); private static Set<Object> candidates = new HashSet<>(); static { candidates.add(DataSourceType.PRODUCTION_DB.type()); candidates.add(DataSourceType.TEST_DB.type()); } public static void setType(String type){ dataSourceHolder.set(type); } public static String getType(){ return dataSourceHolder.get(); } public static void clear(){ dataSourceHolder.remove(); } public static void addCandidates(Set<Object> set) { candidates.addAll(set); } public static boolean support(String type){ return candidates.contains(type); } }
// 测试代码 public class AdminDaoTest extends BaseTest{ @Autowired private AdminDao adminDao; @Test public void test(){ DataSourceHolder.setType(DataSourceType.TEST_DB.type()); Admin admin = (Admin)adminDao.getById(Admin.class, 1); System.out.println(admin); DataSourceHolder.setType(DataSourceType.PRODUCTION_DB.type()); admin = (Admin)adminDao.getById(Admin.class, 1); System.out.println(admin); } } //输出结果 switch_ds:test_db null switch_ds:pro_db Admin [id=1, createTime=1900-01-01 00:00:00.0, name=admin, pswd=MDkwZDA2NmI1NDdkZGM0Y2U2MTBjYmY3ZjA4YWM4MTc=, flag=2, status=0]
通过Aop简化编程式切换
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface TargetDataSource { DataSourceType type(); }
@Component @Aspect @Order(-1) public class DataSourceAspect { @Before("@annotation(ds)") public void before(JoinPoint joinPoint,TargetDataSource ds){ DataSourceType dataSourceType = ds.type(); System.out.println("transform to "+dataSourceType.type()); if(DataSourceHolder.support(dataSourceType.type())){ DataSourceHolder.setType(dataSourceType.type()); } } }
// Spring配置文件中增加如下配置 <aop:aspectj-autoproxy expose-proxy="true"/> <bean class="xx.xx.DataSourceAspect"/>
配置完后只需要在使用方法上添加注解
@TargetDataSource(type=DataSourceType.TEST_DB)或者@TargetDataSource(type=DataSourceType.PRODUCTION_DB)
即可切换到目标数据源。
至此,动态数据源的配置全部结束,相信已经可以测试是否能够动态切换了。那么如果切换失败呢?以下就是若干关于切换失败原因的思考。
动态切换数据源失败的可能和解决方案
我们重新看这张图,在一次数据库操作中,涉及到的模块也就以上这些,那么从后往前一个个确定可能导致切换失败的模块。
transaction(事务)
// AdminDao.java @TargetDataSource(type=DataSourceType.PRODUCTION_DB) @Transactional public void testfail(Integer id){ Session session = getSession(); Admin admin = (Admin) session.get(Admin.class, id); System.out.println(admin); testfail1(id); } @TargetDataSource(type=DataSourceType.TEST_DB) @Transactional public void testfail1(Integer id){ Session session = getSession(); Admin admin = (Admin) session.get(Admin.class, id); System.out.println(admin); }
// 测试代码 // 测试环境db无数据 public class AdminDaoTest extends BaseTest{ @Autowired private AdminDao adminDao; @Test public void test(){ adminDao.testfail1(1); adminDao.testfail(1); } } // 测试结果 transform to test_db switch_ds:test_db null transform to pro_db switch_ds:pro_db Admin [id=1, createTime=1900-01-01 00:00:00.0, name=admin, pswd=MDkwZDA2NmI1NDdkZGM0Y2U2MTBjYmY3ZjA4YWM4MTc=, flag=2, status=0] Admin [id=1, createTime=1900-01-01 00:00:00.0, name=admin, pswd=MDkwZDA2NmI1NDdkZGM0Y2U2MTBjYmY3ZjA4YWM4MTc=, flag=2, status=0]
Step1
相信看到以上结果都知道切换数据源失败了,甚至连在调用自己类方法时aop都失效,并没有调用切面方法。
那么我们一个个解决,首先解决同一个类方法互相调用时发生的aop失效:
// AdminDao.java @TargetDataSource(type=DataSourceType.PRODUCTION_DB) @Transactional public void testfail(Integer id){ Session session = getSession(); System.out.println("session hashcode:"+session.hashCode()); Admin admin = (Admin) session.get(Admin.class, id); System.out.println(admin); ((AdminDao)AopContext.currentProxy()).testfail1(id); } @TargetDataSource(type=DataSourceType.TEST_DB) @Transactional public void testfail1(Integer id){ Session session = getSession(); System.out.println("session hashcode:"+session.hashCode()); Admin admin = (Admin) session.get(Admin.class, id); System.out.println(admin); } // 输出结果 transform to test_db switch_ds:test_db session hashcode:489011343 null transform to pro_db switch_ds:pro_db session hashcode:493857485 Admin [id=1, createTime=1900-01-01 00:00:00.0, name=admin, pswd=MDkwZDA2NmI1NDdkZGM0Y2U2MTBjYmY3ZjA4YWM4MTc=, flag=2, status=0] transform to test_db session hashcode:493857485 Admin [id=1, createTime=1900-01-01 00:00:00.0, name=admin, pswd=MDkwZDA2NmI1NDdkZGM0Y2U2MTBjYmY3ZjA4YWM4MTc=, flag=2, status=0]
再看输出结果,很显然,虽然是方法内部调用,但是aop的增强已经实现,实现了DataSouceHolder的切换,只不过数据源没有切换,仍然是正式环境。那么这就是关键,为什么无法切换数据源呢?
其实答案也很简单,事务环境的隔离性 。
如果在一个方法A中已经开启事务,在这个方法A中调用另一个事务方法B,如果不作其他配置,那么方法B会沿用方法A的事务环境,而不会开启一个新的事务。如果不开启一个新的事务,当然也不会进行一系列的改变直至数据源的切换。这也是为什么后面两个session的hashCode相同的原因。这也证明他们同处于一个事务环境,因为hibernate的session适合transaction绑定的。
Step2
// AdminDao.java @TargetDataSource(type=DataSourceType.PRODUCTION_DB) @Transactional public void testfail(Integer id){ Session session = getSession(); System.out.println("session hashcode:"+session.hashCode()); Admin admin = (Admin) session.get(Admin.class, id); System.out.println(admin); ((AdminDao)AopContext.currentProxy()).testfail1(id); } // 配置事务隔离性为REQUIRES_NEW,调用时必须开启一个新的事务环境 @TargetDataSource(type=DataSourceType.TEST_DB) @Transactional(propagation=Propagation.REQUIRES_NEW) public void testfail1(Integer id){ Session session = getSession(); System.out.println("session hashcode:"+session.hashCode()); Admin admin = (Admin) session.get(Admin.class, id); System.out.println(admin); } // 输出结果 transform to test_db switch_ds:test_db session hashcode:2102414642 null transform to pro_db switch_ds:pro_db session hashcode:15044674 Admin [id=1, createTime=1900-01-01 00:00:00.0, name=admin, pswd=MDkwZDA2NmI1NDdkZGM0Y2U2MTBjYmY3ZjA4YWM4MTc=, flag=2, status=0] transform to test_db switch_ds:test_db session hashcode:186297563 null
从结果中可以看到,切换数据源成功,并且session的hashCode同样更改。从而证明了这种情况下的切换数据源失败是因为事务环境没有改变导致的。
Session
这种情况比较少见,无非就是在同一个session环境下进行了不同的数据库操作。只要保证session都是通过sessionFactory.getCurrentSession();获取,一般都可以避免。
sessionFactory,DataSource
这两部分基本上只要注意动态数据源的key,value对应一般都没有问题。但之前出问题也就是出在这块不可能出问题的地方。
因为本人的项目使用Spring + Hibernate,因此连接池选用proxool。之前的配置是
<bean parent="parentDataSource" id="production_env"> <property name="driverUrl" value="${jdbc.url.local}"/> <property name="password" value="${jdbc.password.local}"/> </bean> <bean parent="parentDataSource" id="test_env"> <property name="driverUrl" value="${jdbc.url.server.test}"/> <property name="password" value="${jdbc.password.server.test}"/> </bean>
这种配置下,虽然aop也生效了,但是没法切换数据源。针对这种情况
<bean parent="parentDataSource" id="production_env"> <property name="driverUrl" value="${jdbc.url.local}"/> <property name="password" value="${jdbc.password.local}"/> <property name="alias" value="proxool.a2"/> </bean> <bean parent="parentDataSource" id="test_env"> <property name="driverUrl" value="${jdbc.url.server.test}"/> <property name="password" value="${jdbc.password.server.test}"/> <property name="alias" value="proxool.a1"/> </bean>
给数据源增添别名即可解决。
总结
以上就是所有关于动态切换数据源的内容,相关知识主要涉及到AOP,事务管理。因此只要掌握相关知识,想来处理起来不算困难。除了本文中由于proxool的小配置别名导致的切换失败的问题…希望能够帮到大家。