一个由session.close()引发的血案

本文分析了一个使用MyBatis框架项目的连接池异常问题,详细讲述了如何定位到未关闭SqlSession导致的问题,并深入探讨了MyBatis内部实现,最终解决了连接池中出现关闭连接异常的情况。
最近接手了个新项目, 项目一直存在使用已关闭的连接 异常问题.
上图:


项目环境:
1.使用的是 mybatis-3.2.6.jar orm框架
2.使用mybatis 默认提供的 pooled  连接池
   配置 详见:

问题分析:
1. 问题猜测:
因为公司连接mysql 中间有层F5的代理,考虑是不是mysql的wait_timeout 已过期,但是F5的与应用的连接还是active状态
    解决方案:
<property name="poolMaximumActiveConnections" value="20"/>
<property name="poolPingQuery" value="SELECT NOW()" />
<property name="poolPingEnabled" value="true" />
<!-- 设置获取连接超时时间 -->
<property name="poolTimeToWait" value="1000" />
<!-- 设置连接最大检测时间 -->
<property name="poolMaximumCheckoutTime" value="1000"/>
<!-- 配置poolPingQuery多次时间被用一次 -->
<property name="poolPingConnectionsNotUsedFor" value="500"/>

修改配置文件中  poolPingConnectionsNotUsedFor 参数使其小于F5的time_out 但问题依旧

2.问题猜测:
线程池实现有bug,导致有极端情况没有判断出连接是否可用,而继续存活于idleConnections 池中
  解决方案:
跟踪源码,打开日志等级为debug.

通过跟踪有问题的connection 的hashcode (554300411) 发现在
其中一段时间中 没有Returned connetion ..



可以发现 正常的checkout - return 都相差不到100ms

而从这条日志开始 checkout 到下一次return 相隔了有5分钟!!!!!

继续排查
cat xxx.log |grep "[03-02 19:12:36 103 DEBUG] [pool-37-thread-1] datasource.pooled.PooledDataSource.popConnection 369 - Checked out connection 554300411 from pool." -A 50 --color


发现执行了 这方法.就再也没有在5分钟之内return poll 了。
开始怀疑是 慢sql 导致的。但是日志可以看出整个查询也就消耗了4ms

next
跟踪代码吧。
SqlSession session = sqlSessionFactory.openSession(true);
....
 catch (Exception e) {
            log.error("xxxx", e);
            response.setResponseCode(ResponseCode.SYSTEM_ERROR.getCode());
            response.setErrorMessage("xxxx");
            throw e;
        }
没有 调用 session.close() !!!!!!!!!!!!
修改掉。好了。问题fix了。
那么问题来了。为什么没有调用session.close()会出现这个问题呢?

================我们来看下 mybatis对于close() 干了什么 =====================



public class DefaultSqlSession implements SqlSession {
 
public void close() {
    try {
      /** 关键 */
      executor.close(isCommitOrRollbackRequired(false));
      dirty = false;
    } finally {
      ErrorContext.instance().reset();
    }
  }
}
public abstract class BaseExecutor implements Executor {

public void close(boolean forceRollback) {
    try {
      try {
        rollback(forceRollback);
      } finally {
        /** 关键 */
        if (transaction != null) transaction.close();
      }
    } catch (SQLException e) {
      // Ignore.  There's nothing that can be done at this point.
      log.warn("Unexpected exception on closing transaction.  Cause: " + e);
    } finally {
      transaction = null;
      deferredLoads = null;
      localCache = null;
      localOutputParameterCache = null;
      closed = true;
    }
  }
}



public class JdbcTransaction implements Transaction {
 
public void close() throws SQLException {
    if (connection != null) {
      resetAutoCommit();
      if (log.isDebugEnabled()) {
        log.debug("Closing JDBC Connection [" + connection + "]");
      }
      /** 关键 */
      connection.close();
    }
  }
}
找到关键了。最终执行了 connection.close(). 那 connection 是由谁生成的呢?
我们从头开始.
SqlSession session = sqlSessionFactory.openSession(false);



public class DefaultSqlSessionFactory implements SqlSessionFactory {
 
  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      /** 关键 */
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      /** 关键 */
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
}

这两段代码就决定了最终使用什么样的datasource
由于前面配置以及告知 使用 <dataSource type="POOLED"> 所以 我们看下 pooledDataSource的实现



/**
* 该类实现了动态代理接口
*/
class PooledConnection implements InvocationHandler {
 
/** 构造器中 注意有个proxyConnection 代理了Connection 使执行connection任何方法
 *  都会执行invoke方法 
 */
 public PooledConnection(Connection connection, PooledDataSource dataSource) {
    this.hashCode = connection.hashCode();
    this.realConnection = connection;
    this.dataSource = dataSource;
    this.createdTimestamp = System.currentTimeMillis();
    this.lastUsedTimestamp = System.currentTimeMillis();
    this.valid = true;
    this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
  }

/**
 * invoke中对close做了特殊处理,是close调用时 执行的是pushConnection操作
 */
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
    if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
      dataSource.pushConnection(this);
      return null;
    } else {
      try {
        if (!Object.class.equals(method.getDeclaringClass())) {
          // issue #579 toString() should never fail
          // throw an SQLException instead of a Runtime
          checkConnection();
        }
        return method.invoke(realConnection, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
  }  
}

public class PooledDataSource implements DataSource {

/** 从 空闲的连接池中获取可用连接 (省部分代码)*/
private PooledConnection popConnection(String username, String password) throws SQLException {
    while (conn == null) {
      synchronized (state) {
        if (state.idleConnections.size() > 0) {
          // Pool has available connection
          conn = state.idleConnections.remove(0);
            ..............
        } else {
          // Pool does not have available connection
          if (state.activeConnections.size() < poolMaximumActiveConnections) {
            // Can create new connection
            conn = new PooledConnection(dataSource.getConnection(), this);
            @SuppressWarnings("unused")
            //used in logging, if enabled
            Connection realConn = conn.getRealConnection();
            if (log.isDebugEnabled()) {
              log.debug("Created connection " + conn.getRealHashCode() + ".");
            }
          } else {
            // Cannot create new connection
            ............
            if (longestCheckoutTime > poolMaximumCheckoutTime) {
              // Can claim overdue connection
              .......
              state.activeConnections.remove(oldestActiveConnection);
              if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                oldestActiveConnection.getRealConnection().rollback();
              }
              /** 这里默认构造器有个小坑 会重置 LastUsedTimestamp  */
              conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
              oldestActiveConnection.invalidate();
            }
          }
        }
        if (conn != null) {
          if (conn.isValid()) {
            if (!conn.getRealConnection().getAutoCommit()) {
              conn.getRealConnection().rollback();
            }
            conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
            conn.setCheckoutTimestamp(System.currentTimeMillis());
            conn.setLastUsedTimestamp(System.currentTimeMillis());
            state.activeConnections.add(conn);
          } else {
            if (log.isDebugEnabled()) {
              log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
            }
            ...........
            conn = null;
            ...........
          }
        }
      }
    }
    .............
    }
    return conn;
  }

/** 把激活连接从 活动连接池中移除 放回到 空闲连接池中 */
protected void pushConnection(PooledConnection conn) throws SQLException {

    synchronized (state) {
      state.activeConnections.remove(conn);
      ..........
      state.idleConnections.add(newConn);
      ............
      }
  }
  
}
问题很明显了。如果不调用close 方法
当前连接不会释放回空闲连接池中,一直存在于 活动连接池中, 当空闲连接池耗光之后 pop 会从 活动连接池 中选取第一个连接
返回给应用.而这个时候 这个连接从最后次使用到拿出来可能过了 好几分钟了。就真的是个处于关闭状态的连接了



<think>好的,我现在需要解决服务端调用session.close时抛出异常的问题。首先,我需要理解session.close的上下文以及可能引发异常的原因。根据用户提供的引用,特别是引用[1]提到Zookeeper的session过期问题,客户端和服务端维持长连接,session id全局唯一,断开后可能重连,服务端保留状态。这让我联想到其他系统如WebSocket或网络服务中的session管理,可能存在类似问题。 接下来,用户的问题是关于服务端调用session.close时抛出异常,需要处理方法、原因和解决方案。我需要结合常见的异常情况来分析。可能的原因包括:1.**重复关闭session**:多次调用close方法导致异常,比如在已经关闭的session上再次关闭。 2. **资源未释放**:session关联的资源(如数据库连接、文件句柄)未正确释放,导致关闭时出现异常。3.**并发操作**:在多线程环境下,其他线程可能正在使用session,此时关闭会导致状态不一致。4.**网络问题**:比如在分布式系统中,关闭session时网络中断,导致服务端无法正常处理请求。 5. **第三方库或框架的限制**:某些框架可能对session的生命周期有特定要求,不遵循规范可能导致异常。根据引用[2]中的WebSocket配置和并发问题,比如加锁同步发送或使用装饰器处理并发,可能类似的解决方案适用于session关闭时的并发问题。例如,在关闭session前确保所有操作已完成,避免竞态条件。另外,引用[3]提到断开连接时处理用户列表,可能涉及在session关闭时清理相关资源。如果清理过程中出现异常,比如访问已释放的资源,也可能导致问题。需要确保在关闭前完成所有必要的清理工作。 解决方案方面,可能需要以下步骤:-**检查session状态**:在关闭前确认session是否已处于关闭状态,避免重复关闭。 -**异常捕获与日志记录**:在close方法周围添加try-catch块,捕获异常并记录详细信息,帮助诊断问题。- **资源释放**:确保所有关联资源在关闭前正确释放,如关闭数据库连接、释放文件锁等。- **同步机制**:在多线程环境下,使用同步锁或原子操作,确保关闭时没有其他线程在操作session。 -**重试机制**:对于网络相关的异常,可以实现重试逻辑,确保在临时故障时能够成功关闭。此外,参考引用[4]中关于Linuxsession的组修改问题,可能需要重新加载配置或重启服务来使变更生效。类比到session关闭,可能需要检查是否有依赖的配置或环境变化导致异常,比如权限变更或资源不可用。需要验证这些可能的原因,并逐一测试解决方案。例如,在代码中添加状态检查,记录关闭时的堆栈跟踪,分析异常类型(如IllegalStateException、IOException等),从而针对性地处理。最后,生成相关问题,帮助用户深入理解可能涉及的方面,如并发控制、资源管理、异常处理策略等。</think>### 服务端 `session.close` 异常处理 #### 常见原因及解决方案 1. **重复关闭 Session** - **原因**:多次调用 `session.close()` 可能导致 `IllegalStateException` 或 `NullPointerException`。 - **解决方案**: - 在关闭前检查 Session 状态: ```java if (session != null && session.isOpen()) { try { session.close(); } catch (Exception e) { // 记录日志 } } ``` - 使用单例模式或原子操作确保仅关闭一次[^1]。 2. **资源未释放导致异常** - **原因**:Session 关联的数据库连接、文件句柄等未释放,关闭时触发资源泄漏异常。 - **解决方案**: - 关闭前显式释放资源: ```java try { // 释放关联资源(如事务、连接池) releaseResources(); session.close(); } catch (ResourceLeakException e) { // 处理泄漏 } ``` - 使用 `try-with-resources` 自动管理资源(若框架支持)[^2]。 3. **并发操作冲突** - **原因**:多线程环境下,其他线程正在读写 Session,关闭时导致状态不一致。 - **解决方案**: - 加锁同步: ```java synchronized (session) { if (session.isOpen()) { session.close(); } } ``` - 使用线程安全的 Session 装饰器(如 `ConcurrentWebSocketSessionDecorator`)[^2]。 4. **网络或服务端异常** - **原因**:分布式系统中,网络中断或服务端状态异常导致关闭失败。 - **解决方案**: - 实现重试机制(如指数退避): ```java int maxRetries = 3; for (int i=0; i<maxRetries; i++) { try { session.close(); break; } catch (NetworkException e) { Thread.sleep(1000 * (i+1)); } } ``` - 监听 Session 状态变更事件,异步处理异常[^3]。 5. **框架或协议限制** - **原因**:特定框架(如 Zookeeper、WebSocket)对 Session 生命周期有严格限制。 - **解决方案**: - 遵循框架规范(如 Zookeeper 的 Session 重连机制[^1])。 - 在 `disconnect` 或 `disconnecting` 事件中清理资源[^3]。 --- #### 通用处理流程 1. **记录详细日志**:捕获异常类型、堆栈及 Session 状态。 2. **状态隔离**:将异常 Session 标记为失效,防止后续操作。 3. **优雅降级**:如返回默认值或触发熔断机制。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值