Spring中多数据库的管理

悬赏:15 发布时间:2008-07-23 提问人:reggie (初级程序员)

情况是这样的:
应用服务器要连接20个数据库,只有1个Postgres数据库是可写的,其余19个Mysql都是只读的。
原来的方案是用XAPool,和Global transaction Manager,管理所有的data source,但连接Postgres数据库时经常扔出链接错误的Exception。
考虑到只有一个数据库可写,于是改用DBCP做data source,配置了一个JDBC transaction manager,管理指向Postgres的data source,其余data source没有配Transaction Manager。发现这样配服务器速度快了很多。
问题出在服务器跑一段时间后,就会发生连接不上Mysql数据库。推测是因为连接池耗尽引起的。查了一下Spring的资料,发现JDBCTemplate在没有transaction manager的情况下应该自己会管理connection。只能推测因为有一个Transaction Manager(针对Postgres的,注入到Manager层,所有的DAO都受其管理),所以jdbctemplate放弃了对connection的管理,而那个Transaction manager 只能管Postgres的连接,导致Mysql的连接没人管。
由于对Spring的机理不熟,以上纯属猜测。有谁能解惑一下?或给个方案?
多谢多谢
该问题已经关闭: 超过15天由系统自动关闭,悬赏平分给所有参与回答的会员

回答

public abstract class JdbcDaoSupport extends DaoSupport {
         ...
	protected final Connection getConnection() throws CannotGetJdbcConnectionException {
		return DataSourceUtils.getConnection(getDataSource());
}
         ...
	}


这个DataSourceUtils.getConnection(getDataSource())具体内容是:
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);


这个TransactionSynchronizationManager.getResource(dataSource)的具体内容是:
	public static Object getResource(Object key) {
		Assert.notNull(key, "Key must not be null");
		Map map = (Map) resources.get();
		if (map == null) {
			return null;
		}
		Object value = map.get(key);
...
}

也就是ThreadLocal中维护了一个Map, key是DataSource Value是ConnectionHolder, ConnectionHolder中有一个被使用过的且会被再使用的Connection. 如果,上面Map中的Connection为空, 就会去dataSource.getConnection(), 并将这个Connection放进新的ConnectionHolder,并将这个ConnectionHolder放进ThreadLocal的Map中.

也就是TransactionSynchronizationManager是支持多DataSource,每个DataSource对应一个Connection的.
lggege (架构师) 2008-07-24
再看
public class DataSourceTransactionManager extends AbstractPlatformTransactionManager implements InitializingBean {
	private DataSource dataSource;
	public DataSource getDataSource() {
		return dataSource;
	}

	protected void doBegin(Object transaction, TransactionDefinition definition) {
...
			if (txObject.isNewConnectionHolder()) {
				TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
			}

	protected void doCleanupAfterCompletion(Object transaction) {
...
DataSourceUtils.releaseConnection(con, this.dataSource);
...
}

doBegin()中会将你所说的Postgres和当前操作的Connection维护在一起,也就是key=Postgres的DataSource,Connection则有可能来自于Mysql或Postgres.


doCleanupAfterCompletion()的DataSourceUtils.releaseConnection具体实现:
		if (dataSource != null) {
			ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
			if (conHolder != null && conHolder.hasConnection() && connectionEquals(conHolder.getConnection(), con)) {
				// It's the transactional Connection: Don't close it.
				conHolder.released();
				return;
			}
		}

也就是会用DataSource去查询Threadlocal的Map,从Map中拿到DataSource对应的Connection, 并将这个Connection释放掉. 当然这个DataSource还是你的Postgres的.
lggege (架构师) 2008-07-24
但回到JdbcDaoSupport的代码:

	protected final Connection getConnection() throws CannotGetJdbcConnectionException {
		return DataSourceUtils.getConnection(getDataSource());
	}

DataSourceUtils.getConnection()的实现是这样的:
		ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
		if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
			conHolder.requested();
			if (!conHolder.hasConnection()) {
				logger.debug("Fetching resumed JDBC Connection from DataSource");
				conHolder.setConnection(dataSource.getConnection());
			}
			return conHolder.getConnection();
		}
		// Else we either got no holder or an empty thread-bound holder here.

		logger.debug("Fetching JDBC Connection from DataSource");
		Connection con = dataSource.getConnection();
...
}


也就是你用Mysql的JdbcDaoSupport去连接Mysql数据库,还是会将Mysql的DataSource和Mysql的Connection放进ThreadLocal的Map中.

问题应该查到了, 由于DataSourceTransactionManager 只会释放他自己的Postgres的DataSource为key的Connection, 但没有释放Mysql对应DataSource为key所对应的Connection.
lggege (架构师) 2008-07-24
不知道是否写明白了.

解决方法: 继承DataSourceTransactionManager, 在事务结束时,将ThreadLocal中的Map的所有DataSource为key对应的所有Connection都关闭掉就可以了.
lggege (架构师) 2008-07-24
注: 完全的理论,没法实践,可能有误. 
lggege (架构师) 2008-07-24
其他只读数据库可不可以做一个同义词(synonym)放在可写的数据库中?
Ben.Sin (初级程序员) 2008-07-24
一个字,牛啊。这回答竟然把spring的bug翻出来了。
llade (资深程序员) 2008-07-24
Iggege看下triggerAfterCompletion和ConnectionSynchronization
triggerAfterCompletion在任何事务结束的情况下被调用,通过ConnectionSynchronization,connection被逐一的销毁。
nihongye (中级程序员) 2008-07-24
不过这里面确实存在问题,如果逐一关闭的过程发生异常,后面没关闭的连接就遗留着了,这种情况,应该有  TransactionSynchronization.afterCompletion threw exception
这样的日志信息
nihongye (中级程序员) 2008-07-24
看到nihongye所指的代码了,  :oops:  丢人了, 回家研究.
lggege (架构师) 2008-07-24
DBCP有一个不被建议使用的叫做abbon..什么的选项,可以设置超时时间,超时时自动回收,能给出一个比较有用的堆栈日志信息(具体怎样配,查一下。。。)
nihongye (中级程序员) 2008-07-24
楼上让我想起一个办法,做一个超时会自动打印出调用堆栈的DataSource
参考http://www.javaeye.com/problems/873

不过做下修正:

public class MyDataSource extends BasicDataSource {
	
	public final static Timer timer = new Timer();
	
	Log logger = LogFactory.getLog(MyDataSource.class);
	class ProxyConnection implements Connection{
		
		Connection innnerConnection;
		String uuid;
		TimerTask task;
		
		public ProxyConnection(Connection conn,String uuid){
			
			final RuntimeException ex = new RuntimeException();
			task=new TimerTask(){

				@Override
				public void run() {
					ex.printStackTrace();
				}
				
			};
			timer.schedule(task, 5000);
			this.innnerConnection=conn;
			this.uuid=uuid;
		}

		public void clearWarnings() throws SQLException {
			this.innnerConnection.clearWarnings();
		}

		public void close() throws SQLException {
			this.task.cancel();
			this.innnerConnection.close();
		}
                  ......

llade (资深程序员) 2008-07-25