目录
本文概览:介绍了超时有关的概念:@Transaction的timeout、mybatis的timeout、mysql的innodb_lock_wait_timeout。
1 问题
1.1 背景
在一个事务中完成解析一个大文件,分批存入到数据库。遇到问题,执行时间比较长,就讨论了事务超时的问题,担心执行时间太长,事务超时自动回滚了。
为了考虑这个问题,需要考虑如下超时相关的设置:
- 一个事务的超时时间。spring的@Transactional
- 一个stametn的执行时间。包括mybais的timeout、jdbc的timeout和mysql中的innodb_lock_wait_timeout
- 一个connection的持续时间。myql中的wait_timeout
1.2 问题的结果
我们目的时时间执行时间过长不会被中止。所以:
- 就不需要设置@Transactional了,因为@Transaction设置的目的是为了在一个事务执行时间超过某一个预知时抛出异常。
- 对于mybatis的timeout也不需要设置,因为这个设置也是为了在某一个statemnet时间太久时被中止。
- 对于myql的innodb_lock_wait_timeout,这个是针对两个事务同时更新同一行记录时,等待的最长时间,因为我们都是新增数据,所以也不需要考虑。
- 对于connection的持续时间wait_timeout。这个时间是这个connection执行最近一个statement到当前的时间间隔,因为我们这个没解析100条记录就执行一次insert,所以两次insert的时间间隔很短,所以也不会超时。
综上,我们不需要额外的进行设置。
2 Spring的 @Transactional的超时
2.1 作用
定义一个事务的超时时间,一个事务中包含很多statement。
2.2 源码实现
1. spring实现超时
(1)根据timeout+当前时间点 赋值给一个deadLine。
(2)每一次执行sql,就会获取到一个statement时,计算liveTime =(deadline- 当前时间),分如下两种情况处理:
- 如果liveTime>0,此时就执行stament.setQueryTimeout(liveTime);
- 如果liveTime < 0,此时就抛出异常
(3)总结
spring实现超时通过deadLine和jdbc的stament#setQueryTime两种策略来判断超时。
2、源码分析
如果选择DataSourceTransactionManager,事务内所有的sql操作必须通过JdbcTemplate执行才能使timeout设置正常工作,通过myBatis执行的sql操作将无法应用超时设置。
(1)在开启事务时将注解中timeout赋值给connectionHolder
1 2 |
DataSourceTransactionManager#doBegin txObject.getConnectionHolder().setTimeoutInSeconds(timeout); |
(2)在ResourceHolderSuport中提供了超时校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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用到了上述接口,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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。
1 2 3 4 5 6 |
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 { } |
2.3 复现@Transactional的超时
由上分析,使用@Transactional的timeout属性时,需要使用JDBCtemplate实现dao,而不能使用Mybatis。JdbcTemplate配置可以参考: JDBCtemplate实例 。
1. 使用JDBC定义一个dao
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@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; } }); } } |
2、设置事务超时的service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@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 2 3 4 5 6 7 |
/** * 测试@Transactional的超时属性---Template */ @Test public void testAnnotationTransactionalServiceTestWithJdbcTemplate(){ timeOutService.testAnnotationTransactionalTimeOutWithJdbcTemplate(); } |
执行结果为
org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Thu Dec 08 10:22:39 CST 2016
3 Mybatis层的timeout
3.1 作用
就是设置一个stament的执行时间,包括update、delte、select和select。
3.2 源码实现
通过jdbc的statement的setQueryTimeout来实现。如下在BaseStatemntHandler中setStatementTimeout来设置超时。
1 2 3 4 5 6 7 8 9 10 |
// 设置超时属性 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.3 复现Mybatis超时
(1)mapper.xml
设置超时时间为“1”。如下
1 2 3 4 5 6 7 8 |
<update id="updateCheckTimeOut" parameterType="int" timeout="1"> UPDATE student SET name = 'timeout' WHERE id = #{id} </update> |
(2)dao接口
1 2 3 4 5 6 7 8 |
@Repository public interface StudentDao { /** * 测试mybatis中timeout字段 * @param id */ void updateCheckTimeOut(int id); } |
(3)service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@Service public class TimeOutService { private static final Logger logger = LoggerFactory.getLogger(TimeOutService.class); @Resource private StudentDao studentDao; /** * 测试mybatis配置的超时,这里还需要开启事务,通过利用不同事务中同时更新同一条语句时,需要等待行锁,来实现增长sql的执行时间 */ @Transactional(value = "transactionManager") public void testMybatisTimeOut() { studentDao.updateCheckTimeOut(1); try { Thread.sleep(3000); logger.info("sleep 3s end"); } catch (Exception e) { } } } |
(4)测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/** * 测试mybais的timeout */ @Test public void testMybatisTimeOut(){ Thread t1 = new Thread(new Runnable() { public void run() { timeOutService.testMybatisTimeOut(); } }); Thread t2 = new Thread(new Runnable() { public void run() { timeOutService.testMybatisTimeOut(); } }); t1.start(); t2.start(); // 为了查看子线程执行情况,所以这里使用while-true while(true){ } } |
结果为:
### SQL: UPDATE student SET name = ‘timeout1’ WHERE id = ?
### Cause: java.sql.SQLException: Query execution was interrupted
4 JDBC的timeout
介绍了JDBC的query timeout 和 socket timeout。
4.1 QueryTiemout
4.1.1 作用
就是设置stament执行时间。
4.1.2 配置
1、 默认情况下为0,即不生效。
2、 设置
通过Statement的setQueryTimeout来设置。如下
1 |
void setQueryTimeout(int seconds) throws SQLException; |
4.1.3 Statement的QueryTimeout处理过程
在执行stament时,会同时建立一个检查超时的线程,如果statment执行超时了,这个线程会向mysql的server端发送一个取消执行的请求,而不是等执行完了statment才统计时间,判断是否超时。具体流程如下:
- 通过调用Connection的createStatement()方法创建statement
- 调用Statement的executeQuery()方法
- statement通过自身connection将query发送给MySQL数据库
- statement创建一个新的timeout-execution线程用于超时处理
- 5.1版本后改为每个connection分配一个timeout-execution线程
- 向timeout-execution线程进行注册
- 达到超时时间
- TimerThread调用JtdsStatement实例中的TsdCore.cancel()方法
- timeout-execution线程创建一个和statement配置相同的connection
- 使用新创建的connection向超时query发送cancel query(KILL QUERY “connectionId”)
4.2 JDBC Socket Timeout
4.2.1 作用
1、作用
在数据库被突然停掉或是发生网络错误(由于设备故障等原因)时,如果没有设置socket timeout的话,应用在数据库返回结果前会无期限地等下去,这种连接被称为dead connection。为了能够避免应用在发生网络错误时产生无休止等待的情况,所以需要设置socket timeout。
注意:socket timeout的值必须要高于statement timeout,否则,在网络正常的情况下,socket timeout将会先生效而statement timeout就失效。
2、socket timeout包含两种timeout:
(1)socket连接时的timeout:connectionTimeout
设置之后会抛出异常 java.net.SocketTimeoutException: connect timed out
(2)socket读写时的timeout:socketTimeout
设置之后会抛出异常 :java.net.SocketTimeoutException: Read timed out
4.2.2 配置
1、默认情况
connectTimeout和socketTimeout都是0,为0时表示不生效。
2、必须需要同时设置connectTimeout和sockTimeout,如下两种方式
(1)通过URL方式设置
1 |
jdbc:mysql://xxx.xx.xxx.xxx:3306/database?connectTimeout=60000&socketTimeout=60000 |
(2)spring在xml时设置dataSource时,如下在配置duridDataSource的connectionTimeout和socketTimeout属性,可以理解为druidDataSource通过这两个属性来拼接成上述URL中的connectionTimeout和socketTimeout。
1 2 3 4 5 6 7 8 9 10 11 |
<bean id="transDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> .......... <property name="connectProperties"> <props> <prop key="connectTimeout">1000</prop> <prop key="socketTimeout">1000</prop> </props> </property> .............. </bean> |
4.2.3 JDBC的conetionTimeout和socketTimeout的底层实现
1、socket连接时的timeout:connectionTimeout。
通过Socket.connect(SocketAddress endpoint, int timeout)设置
1 2 3 4 5 6 7 8 9 10 11 |
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)设置。
1 2 3 4 5 6 7 8 9 |
socket = new Socket(); 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上面。
4.2.4 遇到过的一个问题
1、 问题描述
之前在做一个项目时,程序运行在进行查询数据库时经常hang住了。
2、分析
在hang住的时候通过jstack打印的堆栈信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
"main" prio=10 tid=0x00007ff27400a000 nid=0x7d88 runnable [0x00007ff2792d5000] java.lang.Thread.State: RUNNABLE at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.read(SocketInputStream.java:152) at java.net.SocketInputStream.read(SocketInputStream.java:122) at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:113) at com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:160) at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:188) - locked <0x00000006bca6d6c0> (a com.mysql.jdbc.util.ReadAheadInputStream) at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:2331) at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:2776) at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:2765) at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3301) at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1853) at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1976) at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2497) - locked <0x00000006bca69b00> (a java.lang.Object) at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2426) at com.mysql.jdbc.StatementImpl.executeQuery(StatementImpl.java:1383) - locked <0x00000006bca69b00> (a java.lang.Object) at com.mysql.jdbc.ConnectionImpl.getTransactionIsolation(ConnectionImpl.java:3074) at com.alibaba.druid.filter.FilterChainImpl.connection_getTransactionIsolation(FilterChainImpl.java:347) at com.alibaba.druid.filter.FilterAdapter.connection_getTransactionIsolation(FilterAdapter.java:871) at com.alibaba.druid.filter.FilterChainImpl.connection_getTransactionIsolation(FilterChainImpl.java:344) at com.alibaba.druid.filter.FilterAdapter.connection_getTransactionIsolation(FilterAdapter.java:871) at com.alibaba.druid.filter.FilterChainImpl.connection_getTransactionIsolation(FilterChainImpl.java:344) at com.alibaba.druid.filter.FilterAdapter.connection_getTransactionIsolation(FilterAdapter.java:871) at com.alibaba.druid.filter.FilterChainImpl.connection_getTransactionIsolation(FilterChainImpl.java:344) at com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl.getTransactionIsolation(ConnectionProxyImpl.java:260) |
由上面的堆栈信息,发现是卡在了socketInputStream.read上面,此时只要配置socketTimeout就可以终止hang住的程序了。即通过配置druidDataSource的connectionTimeout和socketTimeout两个属性来实现,此时就不会hang住了,而是会报如下异常:
1 2 3 4 5 6 7 8 9 |
Caused by: java.net.SocketTimeoutException: Read timed out at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.read(SocketInputStream.java:152) at java.net.SocketInputStream.read(SocketInputStream.java:122) at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:100) at com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:143) at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:173) at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:2911) at com.mysql.jdbc.MysqlIO.nextRowFast(MysqlIO.java:2116) |
3、总结
访问数据库时必须配置这两个属性。可以在配置连接池dataSource时,配置connectionTimeout和socketTimeout两个属性来实现。或者直接通过URL指定这两个属性
1 |
jdbc:mysql://xxx.xx.xxx.xxx:3306/database?connectTimeout=60000&socketTimeout=60000 |
5 Mysql层的indb_lock_wait_timeout
5.1 理解
1. 作用
它只针对 行锁(row lock),不同事务同时更新同一行的时候,等待其他事务提交的时间,所以一个事务中如果新增数据,那么不会受到事务超时影响。
2.变量说明
(1)变量说明
- innodb_lock_wait_timeout(超时时间),事务等待行锁的时间。默认是50s.
(2)查看上面的两个变量
1 |
show variables like '%timeout%' |
如下结果
(3)设置超时变量
1 |
set GLOBAL innodb_lock_wait_timeout = 10; |
3. 一个相关的变量
innodb_rollback_on_timeout ,事务等待超时之后,进行回滚的策略。默认是off,表示只回滚到上一个statement,每执行一个sql语句(一个satement)就有一个保存点,此时是回滚到上一个保存点。如果这个值改为on,那此时事务就进行整体回滚,即执行了rollback。
5.2 复现
5.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,就跟原来的事务没有关系了。
5.2.2 在代码中事务超时的复现
1、mysql设置
1 2 |
# 设置超时变量,单位是s set GLOBAL innodb_lock_wait_timeout = 10; |
2、代码
(1)Dao接口
1 2 3 4 |
@Repository public interface StudentDao { void updateById(int id); } |
(2)事务函数
1 2 3 4 5 6 7 8 9 |
/** * 测试事务超时 */ @Transactional(value="transactionManager") public void excutetransactionTimeOut(){ logger.info("f1 beg"); studentDao.updateById(1); while(true){} } |
(3)测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/** * 测试事务超时 */ @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){ } } |
(4)查看连接线程show processlit
(5)等待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
5.3 mysql 其他时间变量的解释
1. wait_timeout
参考:http://blog.csdn.net/cenfei78325747/article/details/7854611
就是一connection的sleep多长时间,通过show processlist来查看一个连接的sleep时间,这个时间是这个connection执行最近一个statement到当前的时间间隔。这个时间就是设置开一个客户端,不做任何操作的时候的连接保持时间。
2. 慢查询查询时间
6 各种TimeOut之间关系
上面结果timeout的关系,可以通过中一个http://www.importnew.com/2466.html图来表示。
(1)Spring、Mybatis和JDBC级别的超时并没有更改mysql的属性。可以查看三者的实现:
- 对于Spring的@transactional是通过timeout属性初始化了一个deadline,每一次创建statment判断deadline是否小于0,如果小于0抛异常;否则通过JDBC的statement#setQueryTimeout来设置超时
- Mybatis的timeout也是通过通过JDBC的statement#setQueryTimeout来设置超时。
- JDBC的timeout,是在stament执行时,开启了一个监听线程,发现超时,就终端当前执行的stament,然后抛异常。
(2)只有mysql层没有超时的情况下。上层的JDBC或者spring层的timeout才有意义。
7 参考文献
【1】JDBC的timeout http://www.importnew.com/2466.html
【2】@Tranasaction 的timeout生效 http://www.cnblogs.com/iceJava/p/4500794.html
【3】java SocketTimout https://my.oschina.net/shipley/blog/715196
(全文完)