蹲厕所的熊

benjaminwhx

JDBC超时问题全面分析

2018-02-01 作者: 吴海旭


  1. 1、问题
  2. 2、真实案例:应用服务器在遭到DDos攻击后无法响应
  3. 3、什么是Transaction Timeout?
    1. 3.1、Spring的@Transactional的超时
      1. 3.1.1、原理
      2. 3.1.2、源码分析
      3. 3.1.3、复现超时场景
      4. 3.1.4、对spring超时的一个错误认识
  4. 4、什么是Statement Timeout
    1. 4.1、Mybatis的timeout
      1. 4.1.1、原理
      2. 4.1.2、源码实现
      3. 4.1.3、配置
    2. 4.2、JDBC的timeout
  5. 5、什么是JDBC的socket timeout?
    1. 5.1、作用
    2. 5.2、JDBC的connectionTimeout和socketTimeout的底层实现
    3. 5.3、操作系统的socket timeout配置
  6. 6、Mysql层的innodb_lock_wait_timeout
    1. 6.1、理解
    2. 6.2、复现
      1. 6.2.1、终端中复现
      2. 6.2.2、代码中复现
  7. 7、mysql 其他时间变量的解释
    1. 7.1、wait_timeout
    2. 7.2、慢查询查询时间
  8. 8、总结
    1. 8.1、各个timeout之间的关系
    2. 8.2、配置
      1. 8.2.1、Transaction Timeout配置
      2. 8.2.2、Statement Timeout配置(mybatis举例)
      3. 8.2.3、Socket Timeout配置
  9. 9、参考文献

1、问题

配置了mybatis的超时,为什么在网络问题时,不起作用?

2、真实案例:应用服务器在遭到DDos攻击后无法响应

在遭到DDos攻击后,整个服务都垮掉了。由于第四层交换机不堪重负,网络变得无法连接,从而导致业务系统也无法正常运转。安全组很快屏蔽了所有的DDos攻击,并恢复了网络,但业务系统却还是无法工作。 通过分析系统的thread dump发现,业务系统停在了JDBC API的调用上。20分钟后,系统仍处于WAITING状态,无法响应。30分钟后,系统抛出异常,服务恢复正常。

为什么我们明明将query timeout设置成了3秒,系统却持续了30分钟的WAITING状态?为什么30分钟后系统又恢复正常了? 当你对理解了JDBC的超时设置后,就能找到问题的答案。

下面我们从3个概念去详解超时问题:Transaction TimeoutStatement TimeoutSocket Timeout

3、什么是Transaction Timeout?

transaction timeout一般存在于框架(Spring, EJB)或应用级。transaction timeout或许是个相对陌生的概念,简单地说,transaction timeout就是“statement Timeout * N(需要执行的statement数量) + @(垃圾回收等其他时间)”。transaction timeout用来限制执行statement的总时长。

3.1、Spring的@Transactional的超时

3.1.1、原理

spring中根据timeout+当前时间点 赋值给一个deadLine。每一次执行sql,就会获取到一个statement时,计算liveTime =(deadline- 当前时间),分如下两种情况处理:

  • 如果liveTime>0,此时就执行stament.setQueryTimeout(liveTime);
  • 如果liveTime < 0,此时就抛出异常

spring实现超时通过 deadLine 和jdbc的 statement#setQueryTime 两种策略来判断超时。

3.1.2、源码分析

如果选择DataSourceTransactionManager,事务内所有的sql操作必须通过JdbcTemplate执行才能使timeout设置正常工作,通过myBatis执行的sql操作将无法应用超时设置。(mybatis3.4.0版本及以后的版本才让事务超时生效,下面在Mybatis的timeout里有讲解)

(1)在开启事务时将注解中timeout赋值给connectionHolder

DataSourceTransactionManager#doBegin
           txObject.getConnectionHolder().setTimeoutInSeconds(timeout);

(2)在ResourceHolderSuport中提供了超时校验

public int getTimeToLiveInSeconds() {
    double diff = ((double) getTimeToLiveInMillis()) / 1000;
    int secs = (int) Math.ceil(diff);
    checkTransactionTimeout(secs <= 0);
    return secs;
}

public long getTimeToLiveInMillis() throws TransactionTimedOutException{
   if (this.deadline == null) { 
       throw new IllegalStateException("No timeout specified for this resource holder");
   }
   long timeToLive = this.deadline.getTime() - System.currentTimeMillis();
   // 校验timeout
   checkTransactionTimeout(timeToLive <= 0);
   return timeToLive;
}
// 如果超时抛出异常
private void checkTransactionTimeout(boolean deadlineReached) throws TransactionTimedOutException {
   if (deadlineReached) {
      setRollbackOnly();
      throw new TransactionTimedOutException("Transaction timed out: deadline was " + this.deadline);
   }
}

(3)查看那些地方用到了上述接口

在DataSourceUtils中的applyTransactionTimeout和applyTiemou用到了上述接口,如下

public static void applyTransactionTimeout(Statement stmt, DataSource dataSource) throws SQLException {
    applyTimeout(stmt, dataSource, 0);
}


public static void applyTimeout(Statement stmt, DataSource dataSource, int timeout) throws SQLException {
    Assert.notNull(stmt, "No Statement specified");
    Assert.notNull(dataSource, "No DataSource specified");
    ConnectionHolder holder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
    if (holder != null && holder.hasTimeout()) {
        // 设置statment的超时时间
        stmt.setQueryTimeout(holder.getTimeToLiveInSeconds());
    }
    else if (timeout > 0) {
        // No current transaction timeout -> apply specified value.
        stmt.setQueryTimeout(timeout);
    }
}

(4)再查看用到DataSourceUtils的applyTimeout和applyTransactionTimeout的地方

  • 用到applyTimeout的地方。JdbcTemplate#applyStatementSettings。
  • 用到applyTransactionTimeout的地方,TransactionAwareDataSourceProxy#TransactionAwareInvocaitonHandler.invoke

(5)暂时没有发现使用TransactionAwareDataSourceProxy的地方,所以只需要再查看使用JdbcTemplate#applyStatementSettings地方:在JdbcTemplate的如下3个函数中使用。所以 可以理解使用@Transactional的timeout的时候,必须要使用jdbcTemplate实现dao,而不能通过mybatis(mybatis 3.4.0以后支持了方式,具体改动可以看4.1.2、源码实现)。

public <T> T execute(CallableStatementCreator csc, CallableStatementCallback<T> action){
}
public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action){
}
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
}

3.1.3、复现超时场景

由上分析,使用@Transactional的timeout属性时,需要使用JDBCTemplate实现dao,而不能使用Mybatis。JdbcTemplate配置可以参考: JDBCTemplate实例 。使用JDBC定义一个dao,并在service中设置事务超时。

@Service
public class StudentDaoWithJdbcTemplate {
    @Resource
    private JdbcTemplate jdbcTemplate;
    public void update() {
        jdbcTemplate.execute(new StatementCallback<Integer>() {
            public Integer doInStatement(Statement stmt) throws SQLException, DataAccessException {
                stmt.execute("update student set name='success511' where id = 1;");
                return 1;
            }
        });
    }
}

@Service
public class TimeOutService {
    private static final Logger logger = LoggerFactory.getLogger(TimeOutService.class);
    @Resource
    private StudentDaoWithJdbcTemplate studentDaoWithJdbcTemplate;

      /**
     * 测试注解@Transactional的超时timeout---jdbcTemlate实现dao
     */
    @Transactional(value = "transactionManager", timeout = 1)
    public void testAnnotationTransactionalTimeOutWithJdbcTemplate() {
        try {
            Thread.sleep(3000);
            logger.info("sleep 3s end");
        } catch (Exception e) {
        }
        studentDaoWithJdbcTemplate.update();
    }
}

3.1.4、对spring超时的一个错误认识

对这个问题可以看开涛的这个链接:Spring事务超时时间可能存在的错误认识

这个问题主要是在开启事务的方法中从前面进入方法到最后一个sql执行结束的时间没有超过事务超时的配置时间,不管在最后一个sql后sleep多长时间,都不会触发spring的超时,这个原理在上面的源码分析里已经知道了,这里就不赘述了。

4、什么是Statement Timeout

statement timeout用来限制statement的执行时长,timeout的值通过调用JDBC的java.sql.Statement.setQueryTimeout(int timeout) API进行设置。不过现在开发者已经很少直接在代码中设置,而多是通过框架来进行设置。

4.1、Mybatis的timeout

4.1.1、原理

就是设置一个statement的执行时间,包括update、delete、insert和select。通过jdbc的statement的setQueryTimeout来实现。

4.1.2、源码实现

如下在BaseStatementHandler中setStatementTimeout来设置超时。

// 设置超时属性(3.4.0之前的版本)
protected void setStatementTimeout(Statement stmt) throws SQLException {
    Integer timeout = mappedStatement.getTimeout();
    Integer defaultTimeout = configuration.getDefaultStatementTimeout();
    if (timeout != null) {
      stmt.setQueryTimeout(timeout);
    } else if (defaultTimeout != null) {
      stmt.setQueryTimeout(defaultTimeout);
    }
}

// 3.4.0之后的版本
protected void setStatementTimeout(Statement stmt, Integer transactionTimeout) throws SQLException {
    Integer queryTimeout = null;
    if (mappedStatement.getTimeout() != null) {
      queryTimeout = mappedStatement.getTimeout();
    } else if (configuration.getDefaultStatementTimeout() != null) {
      queryTimeout = configuration.getDefaultStatementTimeout();
    }
    if (queryTimeout != null) {
      stmt.setQueryTimeout(queryTimeout);
    }
    StatementUtil.applyTransactionTimeout(stmt, queryTimeout, transactionTimeout);
}

4.1.3、配置

(1)mapper.xml中为每个sql指定timeout

<settings>
    <setting name="defaultStatementTimeout" value="1" />
</settings>

(2)在spring中的SqlSessionFactoryBean中配置默认timeout

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dbcpSource"/>
    <property name="mapperLocations" value="classpath:spring/mapping/*.xml"/>
    <property name="typeAliasesPackage" value="com.jd.overseas.iou.dao.jdbc.domain"/>
    <property name="configuration">
      <bean class="org.apache.ibatis.session.Configuration">
        <property name="defaultStatementTimeout" value="3"/>
      </bean>
    </property>
  </bean>

4.2、JDBC的timeout

MySQL JDBC Statement的QueryTimeout处理过程

  • 1 通过调用Connection的createStatement()方法创建statement
  • 2 调用Statement的executeQuery()方法
  • 3 statement通过自身connection将query发送给MySQL数据库
  • 4 statement创建一个新的timeout-execution线程用于超时处理
  • 5 5.1版本后改为每个connection分配一个timeout-execution线程
  • 6 向timeout-execution线程进行注册
  • 7 达到超时时间
  • 8 TimerThread调用JtdsStatement实例中的TsdCore.cancel()方法
  • 9 timeout-execution线程创建一个和statement配置相同的connection
  • 10 使用新创建的connection向超时query发送cancel query(KILL QUERY “connectionId”)

源码分析直接跳这个链接看:[mysql driver]query timeout实现

5、什么是JDBC的socket timeout?

5.1、作用

JDBC的socket timeout在数据库被突然停掉或是发生网络错误(由于设备故障等原因)时十分重要。由于TCP/IP的结构原因,socket没有办法探测到网络错误,因此应用也无法主动发现数据库连接断开。如果没有设置socket timeout的话,应用在数据库返回结果前会无期限地等下去,这种连接被称为dead connection。 为了能够避免应用在发生网络错误时产生无休止等待的情况,所以需要设置socket timeout。

注意:socket timeout的值必须要高于statement timeout,否则,在网络正常的情况下,socket timeout将会先生效而statement timeout就失效。

socket timeout包含两种timeout:

(1)socket连接时的timeout:connectionTimeout
设置之后会抛出异常 java.net.SocketTimeoutException: connect timed out

(2)socket读写时的timeout:socketTimeout
设置之后会抛出异常 :java.net.SocketTimeoutException: Read timed out

5.2、JDBC的connectionTimeout和socketTimeout的底层实现

(1)socket连接时的timeout:connectionTimeout。通过 Socket.connect(SocketAddress endpoint, int timeout) 设置

socket = new Socket();
    long t1 = 0;
    try {
        t1 = System.currentTimeMillis();
        // 设置connect timeout 为2000毫秒
        socket.connect(new InetSocketAddress("www.ss.ssss", 8080), 2000);
    } catch (IOException e) {
        long t2 = System.currentTimeMillis();
        e.printStackTrace();
        System.out.println("Connect failed, take time -> " + (t2 - t1) + "ms.");
    }

(2)socket读写时的timeout:socketTimeout。通过 Socket.setSoTimeout(int timeout) 设置。

 socket.connect(new InetSocketAddress("localhost", 8080));
    // 设置so timeout 为2000毫秒
    socket.setSoTimeout(2000);
    System.out.println("Connected.");
    in = socket.getInputStream();
    System.out.println("reading...");
    t1 = System.currentTimeMillis();
    in.read();

注意:如果不设置此属性,那么如果server端突然中断,那么客户端的就会一直阻塞在in.read上面。

5.3、操作系统的socket timeout配置

如果不设置socket timeout或connect timeout,应用多数情况下是无法发现网络错误的。因此,当网络错误发生后,在连接重新连接成功或成功接收到数据之前,应用会无限制地等下去。但是,通过本文开篇处的实际案例我们发现,30分钟后应用的连接问题奇迹般的解决了,这是因为操作系统同样能够对socket timeout进行配置。公司的Linux服务器将socket timeout设置为了30分钟,从而会在操作系统的层面对网络连接做校验,因此即使JDBC的socket timeout设置为0,由网络错误造成的数据库连接问题的持续时间也不会超过30分钟。

通常,应用会在调用Socket.read()时由于网络问题被阻塞住,而很少在调用Socket.write()时进入waiting状态,这取决于网络构成和错误类型。当Socket.write()被调用时,数据被写入到操作系统内核的缓冲区,控制权立即回到应用手上。因此,一旦数据被写入内核缓冲区,Socket.write()调用就必然会成功。但是,如果系统内核缓冲区由于某种网络错误而满了的话,Socket.write()也会进入waiting状态。这种情况下,操作系统会尝试重新发包,当达到重试的时间限制时,将产生系统错误。在我们公司,重新发包的超时时间被设置为15分钟。

6、Mysql层的innodb_lock_wait_timeout

6.1、理解

1、作用

它只针对 行锁(row lock),不同事务同时更新同一行的时候,等待其他事务提交的时间,所以一个事务中如果新增数据,那么不会受到事务超时影响。

2、变量说明

(1)变量说明

innodb_lock_wait_timeout(超时时间),事务等待行锁的时间。默认是50s.

(2)查看上面的两个变量

show variables like '%timeout%'

如下结果:

(3)设置超时变量

set GLOBAL innodb_lock_wait_timeout = 10;

6.2、复现

6.2.1、终端中复现

  1. 实现复现事务超时

(1)不发生事务超时的情况

开启两个终端,分别执行事务,此时事务2可以执行commit,也可以一直等待下去不会出现事务超时,因为此时事务2都是新增数据,不会涉及到行锁(行锁只发生在更新数据或者删除数据)。

(2)发生事务超时

首先我们开一个终端执行上面事务1。

然后再开一个终端执行事务2,此时执行事务2的时候就会hang住(如上图),一直等待下去,直到事务超时,报错ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction 此时可以继续执行sql,或者执行commit,或者rollbac。如下图是一个完整的例子。

(3)比较 innodb_rollback_on_timeoue为false和ture的两种情况

上面的情况下是在 innodb_rollback_on_timeout=false的情况下进行的,那么 innodb_rollback_on_timeout=true时,则表示回滚了整个事务。此时再执行一个sql,就跟原来的事务没有关系了。

6.2.2、代码中复现

1、mysql设置

# 设置超时变量,单位是s
set GLOBAL innodb_lock_wait_timeout = 10;

2、代码

@Repository
public interface StudentDao {
    void updateById(int id);
}

/**
 * 测试事务超时
 */
@Transactional(value="transactionManager")
public void excutetransactionTimeOut(){
    logger.info("f1 beg");
    studentDao.updateById(1);
    while(true){}
}

/**
 * 测试事务超时
 */
@Test
public void testTransactionTimeOut() {
    Thread t1 = new Thread(new Runnable() {
        public void run() {
            transactionService.executeTransactionTimeOut();
        }
    });
    Thread t2 = new Thread(new Runnable() {
        public void run() {
            transactionService.executeTransactionTimeOut();
        }
    });
    t1.start();
    t2.start();
    while (true){
    }
}

查看连接线程show processlist

等待10s之后,抛出异常如

org.springframework.dao.CannotAcquireLockException:

### Error updating database.  Cause: java.sql.SQLException: Lock wait timeout exceeded; try restarting transaction
### The error may involve dao.datasource1.StudentDao.updateById-Inline
### The error occurred while setting parameters
### SQL: UPDATE         student         SET         name = ‘success24’         WHERE         id = ?
### Cause: java.sql.SQLException: Lock wait timeout exceeded; try restarting transaction

7、mysql 其他时间变量的解释

7.1、wait_timeout

参考:在mysql中connection设置和wait-timeout的设置

就是一connection的sleep多长时间,通过show processlist来查看一个连接的sleep时间,这个时间是这个connection执行最近一个statement到当前的时间间隔。 这个时间就是设置开一个客户端,不做任何操作的时候的连接保持时间。

7.2、慢查询查询时间

8、总结

8.1、各个timeout之间的关系

(1)Spring、Mybatis和JDBC级别的超时并没有更改mysql的属性。可以查看三者的实现:

  • 对于Spring的 @transactional 是通过timeout属性初始化了一个deadline,每一次创建statement判断deadline是否小于0,如果小于0抛异常;否则通过JDBC的 statement#setQueryTimeout 来设置超时
  • Mybatis的timeout也是通过通过JDBC的 statement#setQueryTimeout 来设置超时。
  • JDBC的timeout,是在statement执行时,开启了一个监听线程,发现超时,就终端当前执行的statement,然后抛异常。

(2)只有mysql层没有超时的情况下。上层的JDBC或者spring层的timeout才有意义。

8.2、配置

8.2.1、Transaction Timeout配置

总的默认事务超时时间可以在DataSourceTransactionManager中进行配置

<bean id = "transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dbcpSource" />
    <!-- 单位是秒 -->
    <property name="defaultTimeout" value="100" />
</bean>

(1)使用声明式事务之配置文件

<tx:method name="save*" propagation="REQUIRED" timeout="100" />

(2)使用声明式事务之注解

@Transactional(timeout=10)

(3)使用编程式事务进行代码配置

defaultTransactionDefinition.setTimeout(100);

8.2.2、Statement Timeout配置(mybatis举例)

(1)mybatis-config.xml

<settings>
    <setting name="defaultStatementTimeout" value="1" />
</settings>

(2)spring.xml

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dbcpSource"/>
    <property name="mapperLocations" value="classpath:spring/mapping/*.xml"/>
    <property name="typeAliasesPackage" value="com.jd.overseas.iou.dao.jdbc.domain"/>
    <property name="configuration">
      <bean class="org.apache.ibatis.session.Configuration">
        <property name="defaultStatementTimeout" value="3"/>
      </bean>
    </property>
  </bean>

(3)mapper.xml

<select id="select" timeout="100">
    SELECT 1
</select>

8.2.3、Socket Timeout配置

下面是dbcp连接池举例,dbcp配置地址:BasicDataSource Configuration Parameters

<bean id="dbcpSource" class="org.apache.commons.dbcp2.BasicDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="${jdbc.mysql.url}"/>
    <property name="username" value="${jdbc.mysql.username}"/>
    <property name="password" value="${jdbc.mysql.password}"/>
    <property name="maxTotal" value="10"/>
    <property name="minIdle" value="10"/>
    <property name="initialSize" value="3"/>
    <property name="maxWaitMillis" value="1000"/>
    <property name="timeBetweenEvictionRunsMillis" value="6000"/>
    <property name="minEvictableIdleTimeMillis" value="18000"/>
    <!-- 重点配置 -->
    <property name="defaultQueryTimeout" value="100" />
    <property name="connectionProperties" value="connectTimeout=2000;socketTimeout=10000" />
  </bean>

mysql url配置地址:5.1 Driver/Datasource Class Names, URL Syntax and Configuration Properties for Connector/J

jdbc:mysql://XX/XX?XX&connectTimeout=2000&socketTimeout=2000

9、参考文献

事务超时
JDBC超时原理与设置
[mysql driver]query timeout实现
深入分析JDBC超时机制



坚持原创技术分享,您的支持将鼓励我继续创作!



分享

评论