本文通过分析Spring 事务的源码,说明@Transactional Timeout 参数设置的一些问题。
从问题开始,下面两段代码,事务是否都能正常的回滚?
timeout 参数大于3
代码片段1
@Transactional(rollbackFor = Exception.class , propagation = Propagation.REQUIRED,timeout = 3)
public int timeout(int timeout,int blogId){
jdbcTemplate.execute("DELETE FROM blog where id = "+blogId);
Sleep.second(timeout);
jdbcTemplate.execute("SELECT 1");
return timeout;
}
代码片段2
@Transactional(rollbackFor = Exception.class , propagation = Propagation.REQUIRED,timeout = 3)
public int timeout(int timeout , int blogId){
jdbcTemplate.execute("DELETE FROM blog where id = "+blogId);
Sleep.second(timeout);
return timeout;
}
按照我们的认知,上面的代码都应该因为超时抛出异常从而触发 rollback ,但是实际运行结果是 代码片段2 不会回滚,如果在生产环境意味着数据会真实的被删除。
带着问题来看源码
- 首先我们从Timeout的异常堆栈追踪到异常抛出的代码
- 定位到ResourceHolderSupport#checkTransactionTimeout 方法,查看源码
通过源码可以看到,源码通过一个 dealline (Date) 与当前时间比较来判断是否超时,那么这会就产生了两个疑问:
- dealline 在哪里设置的?
- 什么时候调用checkTransactionTimeout?
- 跟踪 dealline 属性,找到设置的方法 ResourceHolderSupport#setTimeoutInMillis,通过Debug面板,追踪调用堆栈,可以定位到DataSourceTransactionManager#doBegin方法
-
查看 DataSourceTransactionManager#doBegin 方法,定位到设置timeout的代码端,到这里我们第一个问题得到了答案
-
通过ResourceHolderSupport#checkTransactionTimeout堆栈找到调用方法
- DataSourceUtils#applyTimeout
- JdbcTemplate#applyStatementSettings
- JdbcTemplate#execute
从这里就能清楚的知道, checkTransactionTimeout 是在每次执行SQL的时候被调用的,这也就能说明为什么代码片段1可以正常回滚了,因为代码片段1在最后执行了一条无意义的SQL “SELECT 1” 触发了 checkTransactionTimeout 的检查。