蒋彪@南京 2012-11-27
这篇文章其实是想说明以下几个技术问题:
A. web项目connection数据库的root是 http request -> serlvet -> class instance -> connection pool -> tcp connection -> DB
B. Connection pool 的生存周期是如何的, conncetion pool 的instance 的内存空间在哪里?
选用Seasar框架,是因为最近一直在接触它, 但是道理是普适的, spring也类似。
和大多数框架一样,可以选择把connection pool的配置和管理放在WEB容器中,但是这里只谈在框架中做的情况。
和大多数框架也一样,可以支持jdbc事务,也可以支持JTA事务,这里只谈JDBC。
MVC框架暂时先省去不谈,我们直接到访问数据库层的service
和spring类似,从抽象的service的基类S2AbstractService<T>中继承而来
然后就能直接执行如下代码,访问DB
return jdbcManager.selectBySqlFile( XXXXDto.class, "XXX.sql", param).getResultList();
这里的jdbcManager来源于S2AbstractService<T>中的被注入的变量
这里的注入的信息来源于<s2jdbc.dicon>中如下的配置
<component name="jdbcManager" class="org.seasar.extension.jdbc.manager.JdbcManagerImpl"> <property name="maxRows">0</property> <property name="fetchSize">0</property> <property name="queryTimeout">0</property> <property name="dialect">mysqlDialect</property> </component>
关于如何注入,请看如下文章
http://blog.csdn.net/nanjingjiangbiao/article/details/8207601
上面这段执行sql的代码,首先是get到了一个封装好的sql执行instance
在jdbcManagerImpl中,有如下方法
public <T> SqlFileSelect<T> selectBySqlFile(Class<T> baseClass, String path, Object parameter) { return new SqlFileSelectImpl<T>(this, baseClass, path, parameter) .maxRows(maxRows).fetchSize(fetchSize).queryTimeout( queryTimeout); }
这里大量运用了模板技术,构造并且返回了具体的实现类SqlFileSelectImpl,最关键的,构造的时候,把jdbcManagerImpl对象本身传进去
回过头来看JdbcManagerImpl,里面有一个如下的变量
protected DataSource dataSource;
这里的dataSource来源于jdbc.dicon中的如下的注入
<component name="dataSource" class="org.seasar.extension.dbcp.impl.DataSourceImpl"/>
在DataSourceImpl中,有如下的一个变量
private ConnectionPool connectionPool;
它来源于如下的注入
<component name="connectionPool" class="org.seasar.extension.dbcp.impl.ConnectionPoolImpl"> <property name="timeout">600</property> <property name="maxPoolSize">0</property> <property name="allowLocalTx">true</property> <property name="transactionIsolationLevel">@java.sql.Connection@TRANSACTION_READ_COMMITTED</property> <!-- <property name="validationQuery">"select 1"</property> <property name="validationInterval">10000</property> --> <destroyMethod name="close"/> </component>
看到了没有,层层的注入,在这里注入了ConnectionPool
同样,我们打开ConnectionPool,在里面看到如下变量
private XADataSource xaDataSource;
这里是真正的datasource connection data被注入进去了
<component name="xaDataSource" class="org.seasar.extension.dbcp.impl.XADataSourceImpl"> <property name="driverClassName">"com.mysql.jdbc.Driver"</property> <property name="URL"> "jdbc:mysql://xxxxx/xxxx?characterEncoding=UTF-8&characterSetResults=UTF-8&zeroDateTimeBehavior=convertToNull" </property> <property name="user">"root"</property> <property name="password">"123456"</property> </component>
※本文作者蒋彪 原发于http://blog.csdn.net/nanjingjiangbiao 转载请注明
真正的sql的执行发生在AbstactSelect中
public List<T> getResultList() { prepare("getResultList"); logSql(); try { return getResultListInternal(); } finally { completed(); } } => protected T getSingleResultInternal() { ResultSetHandler handler = createSingleResultResultSetHandler(); T ret = null; JdbcContext jdbcContext = jdbcManager.getJdbcContext(); try { ret = (T) processResultSet(jdbcContext, handler); if (disallowNoResult && ret == null) { throw new SNoResultException(executedSql); } } finally { if (!jdbcContext.isTransactional()) { jdbcContext.destroy(); } } return ret; }
上面吸引我的是如下的代码
JdbcContext jdbcContext = jdbcManager.getJdbcContext();
这里拿到了jdbc上下文,具体的代码如下
public JdbcContext getJdbcContext() { JdbcContext ctx = getTxBoundJdbcContext(); if (ctx != null) { return ctx; } Connection con = DataSourceUtil.getConnection(dataSource); if (hasTransaction()) { ctx = createJdbcContext(con, true); setTxBoundJdbcContext(ctx); } else { ctx = createJdbcContext(con, false); } return ctx; }
首先,如下第一行代码,拿到了与当前事务绑定的jdbc上下文, 在web环境下,也就是当前http请求中开启的事务中绑定的jdbc上下文
JdbcContext ctx = getTxBoundJdbcContext(); => protected JdbcContext getTxBoundJdbcContext() { if (hasTransaction()) { final JdbcContextRegistryKey key = createJdbcContextRegistryKey(); final JdbcContext ctx = JdbcContext.class.cast(syncRegistry .getResource(key)); if (ctx != null) { return ctx; } } return null; }
具体的做法是,做一个线程安全的HashMap, 然后根据key(datasourcename),存入当前事务的jdbc上下文, 下次再进来的时候,根据key拿出, 事务结束之后,根据key clear掉
接下来是拿Connection,具体的逻辑发生在DataSourceImpl中
//线程安全 public synchronized ConnectionWrapper checkOut() throws SQLException { //拿到当前thread对应的事务,通过threadLocal对象存储 Transaction tx = getTransaction(); if (tx == null && !isAllowLocalTx()) { throw new SIllegalStateException("ESSR0311", null); } //取当前事务绑定的connection,如果有的话 ConnectionWrapper con = getConnectionTxActivePool(tx); if (con != null) { if (logger.isDebugEnabled()) { logger.log("DSSR0007", new Object[] { tx }); } return con; } //如果所有的connection pool都被用光了,就等待,超过等待时间就抛出异常 long wait = maxWait; while (getMaxPoolSize() > 0 && getActivePoolSize() + getTxActivePoolSize() >= getMaxPoolSize()) { if (wait == 0L) { throw new SSQLException("ESSR0104", null); } final long startTime = System.currentTimeMillis(); try { wait((maxWait == -1L) ? 0L : wait); } catch (InterruptedException e) { throw new SSQLException("ESSR0104", null, e); } final long elapseTime = System.currentTimeMillis() - startTime; if (wait > 0L) { wait -= Math.min(wait, elapseTime); } } //根据当前的事务,从free connection pool中拿出一个connection con = checkOutFreePool(tx); if (con == null) { // 如果取不到,直接从XaDataSource中拿connection con = createConnection(tx); } if (tx == null) { //在没有事务的情况下,将这个con放入activePool setConnectionActivePool(con); } else { //如果有事务,则将该事务和conn绑定到txActivePool TransactionUtil.enlistResource(tx, con.getXAResource()); TransactionUtil.registerSynchronization(tx, new SynchronizationImpl(tx)); setConnectionTxActivePool(tx, con); } con.setReadOnly(readOnly); if (transactionIsolationLevel != DEFAULT_TRANSACTION_ISOLATION_LEVEL) { con.setTransactionIsolation(transactionIsolationLevel); } if (logger.isDebugEnabled()) { logger.log("DSSR0007", new Object[] { tx }); } return con; }
在ConnectionPoolImpl中实现了以下三个数据结构
//用来存放无事务的被使用的conn private Set activePool = new HashSet(); //用来存放有事务的被使用的conn private Map txActivePool = new HashMap(); //用来存放空闲conn的单链表 private SLinkedList freePool = new SLinkedList();
和有些框架不同, Seasar并没有随着容器的启动, 就把所有的connection初期化保存起来,而是通过如下的算法分配
A. 第一次发生conn请求,发现activePool txActivePool freePool 三个数据结构都是null, 用如下代码新建conn
XAConnection xaConnection = xaDataSource.getXAConnection(); Connection connection = xaConnection.getConnection(); ConnectionWrapper con = new ConnectionWrapperImpl(xaConnection, connection, this, tx);
B. 接下来,根据是否有事务,把该conn存入activePool 或者txActivePool 中,如果同一个事务再次请求conn,就从txActivePool 中取得
C. 在connection 结束的时候, conn的结束一般发生在事务的提交,由框架决定
会到conn的包装类ConnectionWrapperImpl中执行以下方法
public void close() throws SQLException { if (closed_) { return; } if (logger_.isDebugEnabled()) { logger_.log("DSSR0002", new Object[] { tx_ }); } if (tx_ == null) { connectionPool_.checkIn(this); } else { connectionPool_.checkInTx(tx_); } }
这里的所谓的checkin,就是指从activePool 或者txActivePool 中删除conn,并且把该conn包装之后塞入freePool 中
也就是说freePool 是随着程序的运行,线形填充的
所有用DI容器的web项目都是这样,无论spring还是seasar:
1. 当web容器启动的时候,你本工程里所有的instance(包括jdbc操作)就已经在单例的模式下完成了初期化,并且被保存在JVM(tomcat用)的堆里
2. http request发起之后, web容器中生成了专门的thread,用来处理这个request。这个thead会到容器中找到url目的地的Java instance chain ,然后读入到当前thread的栈内存中执行
3. 当前thread的java instance chain,如果一直执行,最后在connection pool 中拿connection 。 关键点就在于当前connection pool是线程安全的吗?
searsar框架中,connection pool是在ConnectionPoolImpl中实现的,这个类默认是以单例模式实例化。
也就是说,多个http 处理thread其实,是读取共用一个ConnectionPoolImpl的instance。
在ConnectionPoolImpl的实现中,可以看到所有对conn pool的操作都加了线程锁。
因此,conn pool 在DI容器中是绝对的线程安全。
#以上#