一、代码使用示例:
使用了循环commit
private void update(LocalDate month, final List<PmProfitAnalyseRouteComplex> analyseRoutes) {
TransactionUtils.doInTxn(sqlsession -> {
PmProfitSyncBaseMapper syncBaseMapper = sqlsession.getMapper(PmProfitSyncBaseMapper.class);
//更新底表
ListUtil.split(analyseRoutes, 100).forEach(sub -> {
// 操作数据库
sqlsession.commit();
});
}, ExecutorType.BATCH);
}
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
@Slf4j
public class TransactionUtils {
final static TransactionDefinition DEFAULT_TXN_DEF = TransactionDefinition.withDefaults();
public static void doInTxn(Consumer<SqlSession> doSomething, TransactionDefinition txnDef, ExecutorType executorType) {
PlatformTransactionManager txnManager = ApplicationContextUtils.getContext().getBean(PlatformTransactionManager.class);
SqlSessionFactory sqlSessionFactory = ApplicationContextUtils.getContext().getBean(SqlSessionFactory.class);
TransactionStatus transaction = txnManager.getTransaction(txnDef);//begin transaction manually
SqlSession sqlSession = sqlSessionFactory.openSession(executorType);
try {
doSomething.accept(sqlSession);//expect no exception, only runtime exception will be thrown
sqlSession.commit();
txnManager.commit(transaction);//commit
}catch(Throwable e) {//catch any exception
if(!(e instanceof BusinessException)) {
log.error("catch unexpected exception", e);
}
try {
sqlSession.rollback();
}finally {
txnManager.rollback(transaction);//roll back, if fail, just throw
}
throw e;//throw runtime exception
}
}
public static void doInTxn(Consumer<SqlSession> doSomething, ExecutorType executorType) {
doInTxn(doSomething, DEFAULT_TXN_DEF, executorType);
}
}
二、分析
关键点:
- 先通过Spring的PlatformTransactionManager开启Spring事务(txnManager.getTransaction)。
- 再手动创建MyBatis的SqlSession。
- 业务代码执行后,先sqlSession.commit(),再txnManager.commit(transaction)。
2.1. Spring事务与MyBatis commit的关系
2.1.1 Spring事务的本质
Spring事务(PlatformTransactionManager)会绑定一个数据库连接到当前线程(DataSourceTransactionManager)。只要在Spring事务范围内,所有JDBC操作都在同一个物理连接上,且只有Spring事务commit/rollback时,物理连接才真正commit/rollback。
2.1.2 MyBatis的commit行为
sqlSession.commit() 实际上会调用Executor.commit(),进而调用JDBC的Connection.commit()。 但如果当前连接被Spring事务管理,MyBatis的commit不会真正提交物理事务,只是flush批量SQL到数据库,物理事务还没提交。
相关源码(MyBatis)
SqlSession.commit() → Executor.commit() → Transaction.commit()
如果Connection是Spring托管的,commit()会被Spring拦截,只有Spring事务提交时才真正commit。
相关源码(Spring)
DataSourceTransactionManager会通过ConnectionHolder绑定连接到线程,拦截所有commit/rollback操作,直到Spring事务commit/rollback。
2.2 代码分析
- txnManager.getTransaction():Spring开启事务,绑定连接。
- sqlSessionFactory.openSession():MyBatis获取同一个连接。
- sqlSession.commit():MyBatis flush批量SQL到数据库,但物理事务未提交。
- txnManager.commit(transaction):Spring统一提交物理事务,所有操作才真正生效。
2.3 参考资料/源码入口
MyBatis: SqlSession.commit() → Executor.commit() → Transaction.commit()
Spring: DataSourceTransactionManager#doBegin、doCommit、doRollback
在Spring事务下,MyBatis的commit()只是flush,不会真正提交物理事务。 只有Spring的txnManager.commit()才会最终提交。 你可以放心在分批时多次sqlSession.commit(),它只是将批量SQL及时发送到数据库,减少内存压力,但数据的最终一致性和原子性由Spring事务保证。
三、源码流转详解
3.1 Spring事务管理器获取连接
3.1.1 事务开启
通过DataSourceUtils.getConnection()获取连接,并绑定到当前线程(ThreadLocal)
3.1.2 绑定到线程
// org.springframework.jdbc.datasource.DataSourceTransactionManager
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = DataSourceUtils.getConnection(this.dataSource); // 关键
txObject.setConnectionHolder(new ConnectionHolder(con), true);
// ...设置隔离级别、只读等
}
只要在Spring事务内,所有DataSource获取的Connection都是同一个。
3.2. MyBatis获取连接
3.2.1 SqlSessionFactory.openSession()
// org.springframework.jdbc.datasource.DataSourceUtils
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
return conHolder.getConnection();
}
// 否则新建连接并绑定
}
通过Spring托管的DataSource获取连接,实际拿到的就是Spring事务绑定的Connection。
3.2.2 MyBatis事务工厂
// org.apache.ibatis.session.defaults.DefaultSqlSessionFactory
public SqlSession openSession(ExecutorType execType) {
return openSessionFromDataSource(execType, null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
Transaction tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
}
MyBatis的SpringManagedTransaction会优先从Spring事务管理器获取连接。
3.3. SqlSession.commit()行为
// org.mybatis.spring.transaction.SpringManagedTransaction
public Connection getConnection() throws SQLException {
if (this.connection == null) {
this.connection = DataSourceUtils.getConnection(this.dataSource);
}
return this.connection;
}
- 实际调用Executor.commit(),最终调用Transaction.commit()。
// org.apache.ibatis.session.defaults.DefaultSqlSession
public void commit(boolean force) {
executor.commit(isCommitOrRollbackRequired(force));
dirty = false;
}
- 如果是Spring托管的事务,这里什么都不做,只是flush批量SQL到数据库,物理事务不提交。
3.4. Spring事务提交
// org.mybatis.spring.transaction.SpringManagedTransaction
public void commit() throws SQLException {
// Do nothing, let Spring manage the transaction.
}
- 只有Spring事务管理器commit时,才会真正调用Connection.commit()。
3.5、流程总结
Spring事务开始,绑定Connection到当前线程。
MyBatis获取SqlSession时,通过Spring托管的DataSource拿到同一个Connection。
SqlSession.commit()只flush批量SQL,不会物理提交事务。
只有Spring事务commit时,才会真正提交数据库事务。
关键源码片段
Spring事务管理器获取连接:DataSourceUtils.getConnection()
MyBatis事务工厂获取连接:SpringManagedTransaction.getConnection()
MyBatis commit行为:SpringManagedTransaction.commit()
Spring事务提交:DataSourceTransactionManager.doCommit()
核心原理总结
Spring事务管理器通过ThreadLocal绑定Connection,保证同一事务内所有操作用同一个连接。
MyBatis与Spring集成时,commit只flush,最终提交由Spring统一控制。
分批commit在MyBatis只是批量SQL的flush,数据原子性和一致性由Spring事务保证。
四、如果事务被spring管理,那么在flush是怎么把sql发到mysql的呢
4.1. flush 到底做了什么?
以批量模式(ExecutorType.BATCH)为例,sqlSession.commit()的本质是:
- 调用MyBatis的Executor.flushStatements(),把缓存的SQL批量发送到数据库(JDBC的addBatch/executeBatch)。
- 但不做Connection.commit(),事务依然由Spring控制。
4.1.1 SqlSession.commit()
// org.apache.ibatis.session.defaults.DefaultSqlSession
public void commit(boolean force) {
try {
executor.commit(isCommitOrRollbackRequired(force));
dirty = false;
} catch (Exception e) {
// ...
}
}
4.1.2 Executor.commit()
// org.apache.ibatis.executor.BaseExecutor
public void commit(boolean required) throws SQLException {
flushStatements();
if (required) {
transaction.commit(); // 这里的commit在Spring托管下不会真正提交
}
}
4.1.3 BatchExecutor.flushStatements()
// org.apache.ibatis.executor.BatchExecutor
public List<BatchResult> flushStatements() throws SQLException {
// 遍历所有缓存的Statement,调用executeBatch()
for (Statement stmt : statementList) {
stmt.executeBatch(); // 这里真正把SQL发到MySQL
}
// ...
}
4.1.4 Spring托管下的Connection
- MyBatis获取Connection时,实际调用的是Spring的DataSourceUtils.getConnection(),返回的是同一个被Spring事务管理的Connection。
- 只有Spring事务提交时,才会调用Connection.commit()。
过程总结
- 调用sqlSession.commit()时,MyBatis会把缓存的SQL通过JDBC的executeBatch()发送到MySQL,但不会提交事务。
- Spring事务提交时,才会真正Connection.commit(),把所有操作原子性提交。
- 如果Spring事务回滚,则Connection.rollback(),所有已发送但未提交的SQL都会被撤销。
五、源码示例Spring管理了Mybatis的事务
5.1 业务层代码示例
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional // Spring事务注解
public void batchInsertUsers(List<User> users) {
for (User user : users) {
userMapper.insertUser(user); // MyBatis底层会addBatch
}
// 这里MyBatis的Executor会在合适时机调用doFlushStatements -> executeBatch
}
}
5.2. Spring事务管理源码流程
5.2.1 AOP拦截事务方法
Spring 事务的入口在 TransactionInterceptor:
// org.springframework.transaction.interceptor.TransactionInterceptor
public Object invoke(MethodInvocation invocation) throws Throwable {
// 1. 获取事务管理器,开启事务
TransactionInfo txInfo = createTransactionIfNecessary(...);
Object retVal;
try {
// 2. 执行业务方法
retVal = invocation.proceed();
} catch (Throwable ex) {
// 3. 异常时回滚
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
// 4. 清理事务信息
cleanupTransactionInfo(txInfo);
}
// 5. 正常时提交
commitTransactionAfterReturning(txInfo);
return retVal;
}
5.2.2 事务管理器设置 autoCommit
以 DataSourceTransactionManager 为例:
// org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin
protected void doBegin(Object transaction, TransactionDefinition definition) {
Connection con = ... // 获取连接
if (con.getAutoCommit()) {
con.setAutoCommit(false); // 关键:Spring开启事务时设置为false
}
// ... 其他事务属性设置
}
5.3 MyBatis批量执行源码片段
// org.apache.ibatis.executor.BatchExecutor#doFlushStatements
for (Statement stmt : statementList) {
// ...
batchResult.setUpdateCounts(stmt.executeBatch()); // 这里发送SQL到数据库
// ...
}
5.4 事务提交/回滚源码
// org.springframework.jdbc.datasource.DataSourceTransactionManager#doCommit
protected void doCommit(DefaultTransactionStatus status) {
status.getConnection().commit(); // 事务方法正常结束时提交
}
// org.springframework.jdbc.datasource.DataSourceTransactionManager#doRollback
protected void doRollback(DefaultTransactionStatus status) {
status.getConnection().rollback(); // 事务方法异常时回滚
}
5.5 JDBC原生行为
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false); // Spring帮你做了这一步
PreparedStatement ps = conn.prepareStatement("INSERT INTO ...");
ps.setString(1, ...);
ps.addBatch();
// ... 多次addBatch
ps.executeBatch(); // 发送SQL,但未提交
conn.commit(); // Spring帮你做了这一步
// 或 conn.rollback(); // Spring帮你做了这一步
5.6 总结流程图
- Spring事务管理器负责设置 autoCommit=false,并在方法结束时决定 commit/rollback。
- MyBatis批量执行只负责把SQL打包发送,是否生效由事务控制。
- 源码层面,事务的开启、提交、回滚都在 Spring 的事务管理器里完成。
六、通过sqlSessionFactory.openSession(executorType)开启的是DefaultSqlSession还是SqlSessionTemplate呢?
在Spring与MyBatis整合后,业务代码中实际获取和使用的是 SqlSessionTemplate,而不是原生的 DefaultSqlSession。
6.1. 关系梳理
- MyBatis原生: 直接用SqlSessionFactory.openSession()获取的是DefaultSqlSession。
- Spring整合后: Spring通过@Autowired、@MapperScan等方式注入的SqlSession,实际是SqlSessionTemplate。
6.2. 为什么是SqlSessionTemplate?
- SqlSessionTemplate是MyBatis-Spring模块提供的线程安全、自动管理事务的SqlSession实现。
- 它内部持有SqlSessionFactory,每次操作时会自动获取、释放底层的DefaultSqlSession,并与Spring事务同步。
6.2.1 SqlSessionTemplate的构造
public class SqlSessionTemplate implements SqlSession {
private final SqlSessionFactory sqlSessionFactory;
// ...
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
// ...
}
}
6.2.2 每次操作时的代理
public <T> T selectOne(String statement, Object parameter) {
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory);
try {
return sqlSession.selectOne(statement, parameter);
} finally {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
- 每次方法调用,都会从SqlSessionFactory获取一个DefaultSqlSession,用完即释放。
6.3. Spring注入的Mapper代理
你在Spring中注入的Mapper接口(如@Autowired UserMapper userMapper), 实际是由MapperFactoryBean生成的JDK动态代理,代理逻辑最终会委托给SqlSessionTemplate。
总结
- Spring+MyBatis整合后,所有Mapper操作、事务管理,都是基于SqlSessionTemplate实现的。
- 手动new/open的才是DefaultSqlSession,但在Spring环境下几乎不会直接用到。
七、那spring为什么要用SqlSessionTemplate呢
本质上,是为了解决线程安全、事务同步、资源管理等一系列与Spring集成时的核心问题。
7.1. 线程安全
- DefaultSqlSession不是线程安全的,每个线程都要有自己的SqlSession实例。
- SqlSessionTemplate是线程安全的,可以被多个线程共享,每次操作时自动获取/释放底层SqlSession。
7.2. 事务同步
- Spring的声明式事务(如@Transactional)需要所有数据库操作都在同一个事务(同一个Connection)中。
- SqlSessionTemplate会自动检测当前线程是否有Spring事务,有则复用同一个Connection,保证MyBatis和Spring事务同步。
7.3. 资源自动管理
- SqlSessionTemplate每次操作后会自动关闭SqlSession,防止资源泄漏。
- 如果直接用DefaultSqlSession,开发者容易忘记关闭,导致连接泄漏。
7.4. Mapper代理无缝集成
- Spring通过MapperFactoryBean和MapperScannerConfigurer自动为Mapper接口生成代理,底层全部委托给SqlSessionTemplate。
- 这样你只需注入Mapper接口,Spring自动帮你完成所有底层细节。
7.5. 统一异常转换
- SqlSessionTemplate会把MyBatis的异常统一转换为Spring的DataAccessException体系,方便全局异常处理。
7.6. 关键源码片段
7.6.1 线程安全实现
public class SqlSessionTemplate implements SqlSession {
// 每次方法调用都新建/获取SqlSession,保证线程安全
public <T> T selectOne(String statement, Object parameter) {
SqlSession sqlSession = getSqlSession(this.sqlSessionFactory);
try {
return sqlSession.selectOne(statement, parameter);
} finally {
closeSqlSession(sqlSession, this.sqlSessionFactory);
}
}
}
7.6.2 事务同步
// org.mybatis.spring.SqlSessionUtils
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory) {
// 1. 检查当前线程是否有已绑定的SqlSessionHolder
// 2. 有则复用,无则新建
}
八:不进行sqlSession.commit();直接使用txnManager.commit(transaction);会进行提交吗(第一部分代码示例)
代码逻辑
SqlSession sqlSession = sqlSessionFactory.openSession(executorType);
// ...
doSomething.accept(sqlSession);
// sqlSession.commit();
txnManager.commit(transaction);
8.1 两者的区别
- sqlSession.commit():只针对当前 SqlSession,会触发 MyBatis 的批量 SQL 执行(如 doFlushStatements),并提交 Connection。
- txnManager.commit(transaction):Spring 统一管理的事务提交,最终也是调用 Connection 的 commit()。
8.2. 如果不调用 sqlSession.commit(),只调用 txnManager.commit(transaction) 会怎样?
1)ExecutorType.SIMPLE
- 如果你用的是 ExecutorType.SIMPLE(非批量),MyBatis 每次操作后会自动 flush,txnManager.commit() 会提交所有已执行的 SQL,数据会被提交。
2)ExecutorType.BATCH
- 如果你用的是 ExecutorType.BATCH(批量执行),MyBatis 会缓存 SQL 到本地,只有在 sqlSession.commit() 或 sqlSession.close() 时才会真正调用 executeBatch() 把 SQL 发送到数据库。
- 如果你不调用 sqlSession.commit(),直接 txnManager.commit(),MyBatis 缓存的批量 SQL 可能不会被发送到数据库,导致数据未写入!
8.2.1 参考 MyBatis 源码(BatchExecutor):
- doFlushStatements 只会在 commit 或 close 时被调用。
8.2.1.1 SimpleExecutor 的 doUpdate
// org.apache.ibatis.executor.SimpleExecutor
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
// 这里会预编译SQL
stmt = prepareStatement(handler, ms.getStatementLog());
// 这里直接执行SQL
return handler.update(stmt);
} finally {
closeStatement(stmt);
}
}
8.2.1.2 StatementHandler 的 update
以 PreparedStatementHandler 为例:
// org.apache.ibatis.executor.statement.PreparedStatementHandler
@Override
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute(); // 这里直接执行SQL
int rows = ps.getUpdateCount();
// ... 省略处理自增主键等
return rows;
}
8.2.2 总结
- SIMPLE模式下,每次调用 update/insert/delete,都会直接执行SQL,不会缓存。
- flushStatements 在 SIMPLE 模式下其实什么都不做
// org.apache.ibatis.executor.SimpleExecutor
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) {
// nothing to flush
return Collections.emptyList();
}
九、BatchExecutor的doUpdate是什么时候调用的,doFlushStatements什么时候调用的,对statement重用是怎么样做的?statement不重用会如何
9.1. BatchExecutor#doUpdate 何时调用?
- doUpdate 是 MyBatis 执行 insert、update、delete 时,最终调用的底层方法。
- 你在 Mapper 层调用 insert/update/delete,会经过 SqlSession → Executor → doUpdate。
- 只要你执行 DML 操作,都会调用到 doUpdate。
9.2. doFlushStatements 何时调用?
- doFlushStatements 负责将缓存的批量 SQL 一次性发送到数据库(即调用 executeBatch())。
- 典型调用时机有:
- 事务提交/回滚时:Spring 事务提交时会调用 MyBatis 的 commit,进而调用 doFlushStatements。
- SQL模板变化时:如果批量中遇到不同的 SQL(如表名/字段不同),MyBatis 会自动 flush 之前的批量。
- 手动调用 sqlSession.commit() 时:也会触发 flush。
9.3. Statement 重用机制
9.3.1 设计目标
同一 SQL 模板(即 SQL 语句结构完全一样)会重用同一个 PreparedStatement,只变参数。 不同 SQL 模板,会新建新的 PreparedStatement。
9.3.2 源码分析
1)doUpdate 关键源码
// org.apache.ibatis.executor.BatchExecutor#doUpdate
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
final Configuration configuration = ms.getConfiguration();
final StatementHandler handler = configuration.newStatementHandler(...);
final BoundSql boundSql = handler.getBoundSql();
final String sql = boundSql.getSql();
Statement stmt;
if (sql.equals(currentSql) && ms.equals(currentStatementMs)) {
// SQL模板和MappedStatement都一样,重用Statement
int last = statementList.size() - 1;
stmt = statementList.get(last);
applyTransactionTimeout(stmt);
handler.parameterize(stmt);
batchResultList.get(last).addParameterObject(parameter);
} else {
// SQL模板变了,先flush之前的批量
Connection connection = getConnection(ms.getStatementLog());
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
currentSql = sql;
currentStatementMs = ms;
statementList.add(stmt);
batchResultList.add(new BatchResult(ms, sql, parameter));
}
handler.batch(stmt);
return BATCH_UPDATE_RETURN_VALUE;
}
2)doFlushStatements 关键源码
// org.apache.ibatis.executor.BatchExecutor#doFlushStatements
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
// 遍历所有缓存的Statement,调用executeBatch
for (int i = 0, n = statementList.size(); i < n; i++) {
Statement stmt = statementList.get(i);
batchResult.setUpdateCounts(stmt.executeBatch());
// 处理主键生成等
closeStatement(stmt);
results.add(batchResult);
}
// 清理缓存
statementList.clear();
batchResultList.clear();
currentSql = null;
return results;
}
9.4. 如果 Statement 不重用会怎样?
每次都新建 PreparedStatement,会导致:
- 数据库端频繁创建/销毁 Statement,性能大幅下降。
- 失去批量执行的优势,每条 SQL 都单独执行,网络和数据库压力大。
- MyBatis 通过 SQL 模板和 MappedStatement 判断是否重用,只有完全一样才重用。
9.5. 总结
- doUpdate:每次 DML 操作都会调用,负责缓存/重用 Statement。
- doFlushStatements:在事务提交、SQL模板变化、手动commit时调用,负责批量执行并清理缓存。
- Statement重用:只有SQL模板和MappedStatement都一样才重用,否则flush并新建。
- 不重用的后果:性能大幅下降,失去批量优势。
十、mybatis中超时时间的设置
在BaseExecutor中
/**
* Apply a transaction timeout.
* @param statement a current statement
* @throws SQLException if a database access error occurs, this method is called on a closed <code>Statement</code>
* @since 3.4.0
* @see StatementUtil#applyTransactionTimeout(Statement, Integer, Integer)
*/
protected void applyTransactionTimeout(Statement statement) throws SQLException {
StatementUtil.applyTransactionTimeout(statement, statement.getQueryTimeout(), transaction.getTimeout());
}
/**
* Apply a transaction timeout.
* <p>
* Update a query timeout to apply a transaction timeout.
* </p>
* @param statement a target statement
* @param queryTimeout a query timeout
* @param transactionTimeout a transaction timeout
* @throws SQLException if a database access error occurs, this method is called on a closed <code>Statement</code>
*/
public static void applyTransactionTimeout(Statement statement, Integer queryTimeout, Integer transactionTimeout) throws SQLException {
if (transactionTimeout == null){
return;
}
Integer timeToLiveOfQuery = null;
if (queryTimeout == null || queryTimeout == 0) {
timeToLiveOfQuery = transactionTimeout;
} else if (transactionTimeout < queryTimeout) {
timeToLiveOfQuery = transactionTimeout;
}
if (timeToLiveOfQuery != null) {
statement.setQueryTimeout(timeToLiveOfQuery);
}
}
代码逻辑解释
public static void applyTransactionTimeout(Statement statement, Integer queryTimeout, Integer transactionTimeout) throws SQLException {
if (transactionTimeout == null){
return; // 没有事务超时要求,直接返回
}
Integer timeToLiveOfQuery = null;
if (queryTimeout == null || queryTimeout == 0) {
// 如果没有单独设置SQL超时,就用事务超时
timeToLiveOfQuery = transactionTimeout;
} else if (transactionTimeout < queryTimeout) {
// 如果事务剩余时间比SQL超时还短,取事务剩余时间
timeToLiveOfQuery = transactionTimeout;
}
if (timeToLiveOfQuery != null) {
// 设置最终的超时时间
statement.setQueryTimeout(timeToLiveOfQuery);
}
}
10.1. JDBC标准行为
1)setQueryTimeout 的作用
- Statement.setQueryTimeout(int seconds) 是JDBC标准API。
- 它的作用是:设置这个Statement对象后续每次执行SQL时的超时时间。
- 只要你在Statement对象上设置过一次,只要Statement没被关闭,这个超时设置会一直生效。 2)官方文档说明
- JDBC官方文档:docs.oracle.com/javase/8/do…
- 说明:Sets the number of seconds the driver will wait for a Statement to execute. If the limit is exceeded, a SQLException is thrown.
10.2. MyBatis源码流程
1)doUpdate 阶段
// org.apache.ibatis.executor.BatchExecutor#doUpdate
if (sql.equals(currentSql) && ms.equals(currentStatement)) {
// 复用Statement
stmt = statementList.get(last);
applyTransactionTimeout(stmt); // 这里设置超时
handler.parameterize(stmt);
...
} else {
// 新建Statement
stmt = handler.prepare(connection, transaction.getTimeout());
applyTransactionTimeout(stmt); // 这里设置超时
handler.parameterize(stmt);
...
}
handler.batch(stmt); // addBatch
2)doFlushStatements 阶段
// org.apache.ibatis.executor.BatchExecutor#doFlushStatements
for (int i = 0, n = statementList.size(); i < n; i++) {
Statement stmt = statementList.get(i);
batchResult.setUpdateCounts(stmt.executeBatch()); // 这里才真正执行SQL
// 如果SQL执行超时,这里会抛SQLException
closeStatement(stmt);
}
10.3. 流程解释
- doUpdate 阶段:只是创建/复用Statement,并设置超时时间(setQueryTimeout),然后addBatch缓存SQL和参数。
- doFlushStatements 阶段:遍历所有Statement,调用executeBatch(),此时如果SQL执行超时,JDBC驱动会根据之前设置的超时时间自动抛出异常。
10.4 为什么叫“提前设置,flush时生效”?
- “提前设置”指的是:在addBatch阶段就把超时时间设置到Statement对象上。
- “flush时生效”指的是:只有在executeBatch真正执行SQL时,JDBC驱动才会用到这个超时设置,如果SQL执行超过这个时间,JDBC会抛异常。
10.5. 结论
- setQueryTimeout是设置在Statement对象上的属性,只要Statement没被关闭,这个属性一直有效。
- MyBatis在doUpdate阶段设置超时,是为了保证后续flush时,所有批量SQL都能受超时保护。
- flush时(executeBatch)才真正用到这个超时设置,此时如果SQL执行超时,JDBC会自动抛异常
十一: 写在最后: 批量提交虽好,但是不要滥用,一直不提交事务,但是也会发送SQL到数据库,数据库会写各种日志文件,也会有开销。并且如果数据库为主从同步,事务过大超过一定限制,可能会造成从库奔溃,因为从库是在主库真正提交事务后才同步,有个参数设置若事务过大则奔溃不同步了。
11.1. Spring事务未提交时,MySQL在做什么?
- 当你用Spring事务(@Transactional)+ MyBatis批量执行时,所有SQL会被发送到MySQL并执行,但只是在当前会话(Connection)里生效,对其他连接不可见。
- 只要你不提交事务(commit),MySQL就不会把这些更改“永久”写入表数据文件,但会做如下操作:
MySQL的内部行为:
- 1.写Undo日志(回滚日志) 每执行一条DML(insert/update/delete),InnoDB都会记录一份undo日志,用于回滚和MVCC。
- 2.写Redo日志(重做日志) 变更会先写到redo log(WAL机制),保证崩溃恢复。
- 3.锁定相关行或表 事务未提交前,相关数据会被锁定,防止其他事务修改。
- 4.binlog(主从同步日志) 只有在commit时,MySQL才会把本次事务的所有操作写入binlog,主从同步才会复制这些操作。
11.2. 事务过大对MySQL主从同步的影响
1)大事务的危害 占用大量内存和磁盘空间
- undo/redo log会持续增长,直到事务提交或回滚。
锁定大量数据
- 可能导致其他事务阻塞,甚至死锁。
主从延迟严重
- binlog只有在commit时才写入,主库长事务未提交,从库就一直收不到这批数据,主从延迟会急剧增加。
崩溃恢复变慢
-大事务未提交时宕机,恢复时要回滚大量操作,耗时很长。
2)主从同步的具体表现
主库执行大事务,长时间不提交
- 主库binlog不会刷新这批操作,从库就同步不到。
- 一旦主库commit,binlog瞬间写入大量数据,从库短时间内要apply大量变更,主从延迟暴涨。
极端情况下,主从断开
- 如果binlog太大、从库apply太慢,可能导致主从复制中断。
11.3. 生产建议
- 避免超大事务:批量操作建议分批(如每1000条commit一次)。
- 合理设置超时时间:防止长事务占用资源。
- 监控主从延迟:发现延迟暴涨要排查大事务。
- MySQL参数优化:如innodb_log_file_size、max_allowed_packet等。
11.4. 总结
Spring事务未提交时,MySQL会持续写undo/redo log,锁定数据,但binlog不会写入,主从同步不会推进。 大事务会导致主从延迟、资源占用、甚至主从断开。 生产环境应避免超大事务,分批提交是最佳实践。
十二、设置的这个时间和数据库连接池超时时间以哪个为准
12.1. 各种超时的定义
1)setQueryTimeout(SQL超时)
- 通过Statement.setQueryTimeout(seconds)设置。
- 控制单条SQL语句的最大执行时间,超时后JDBC驱动会尝试中断SQL并抛出异常。
- 只影响SQL执行,不影响连接本身的生命周期。
2)事务超时
- 由Spring事务管理器等设置(如@Transactional(timeout=30))。
- 控制整个事务的最大存活时间,超时后Spring会强制回滚事务。 3)数据库连接池超时
主要有两类:
- 连接获取超时(如HikariCP的connectionTimeout):获取连接时的最大等待时间。
- 连接最大存活时间/空闲超时(如HikariCP的maxLifetime、idleTimeout):连接在池中存活或空闲的最大时间,超时后连接会被回收。
- 影响连接对象的生命周期,与SQL执行无直接关系。
12.2. 三者的优先级和影响范围
12.3. 哪个为准?怎么分析?
1)SQL执行时,优先以最先触发的超时为准
- setQueryTimeout:如果SQL执行超过这个时间,JDBC会尝试中断SQL,抛出异常。
- 事务超时:如果事务整体超时,Spring会回滚事务,抛出异常。
- 连接池超时:如果连接被连接池回收(如maxLifetime到期),可能导致正在执行的SQL被强制中断或连接失效,抛出连接异常。
2)实际优先级分析
- 一般情况下,setQueryTimeout和事务超时更容易先触发,因为它们的时间通常比连接池的maxLifetime短。
- 如果连接池的maxLifetime设置得很短,有可能在SQL或事务还没结束时,连接就被回收,导致SQL异常中断。 3)典型分析流程
- SQL执行时,先看setQueryTimeout,如果超时,SQL会被中断。
- 事务执行时,如果事务超时,Spring会回滚事务,所有未提交的SQL都会被撤销。
- 连接池层面,如果连接被回收,所有正在用这个连接的操作都会失败,抛出连接异常。
12.4. 实际生产建议
- setQueryTimeout ≤ 事务超时 ≤ 连接池maxLifetime
这样可以保证:
- SQL不会执行超过事务允许的时间。
- 事务不会超过连接的最大存活时间,避免连接被回收时事务还没结束。
- 例子
setQueryTimeout = 30秒、事务超时 = 35秒、连接池maxLifetime = 60秒
12.5. 总结
- 哪个为准? 最先触发的那个为准,即哪个超时先到,哪个就会中断当前操作。
- 如何分析? 结合业务需求,合理设置三者的超时时间,优先保证SQL和事务的超时都小于连接池的maxLifetime,避免连接被提前回收导致异常。
十三、jdbc的rewriteBatchedStatements=true提高执行效率
结论:
- MyBatis的BatchExecutor核心作用:①重用相同SQL模板(预编译语句)的Statement对象,避免频繁创建/销毁Statement。②将多次addBatch的参数缓存到JDBC的Statement对象中,等flush时统一调用executeBatch()
- 调用jdbc发送到数据库取决于jdbc本身,在设置了rewriteBatchedStatements会进行合并发送,但是合并的多了也会出问题,因此加了rewriteBatchedStatements=true后,JDBC会合成一条大SQL,但参数太多/SQL太长还是会出问题。驱动会自动分批,但极端情况下还是可能报错。最佳实践:分批addBatch(如每500~1000条)flush一次,并关注MySQL的max_allowed_packet等参数
13.1. MyBatis的BatchExecutor批量 VS JDBC驱动的批量
MyBatis的BatchExecutor批量
- 只是在Java内存中缓存多次addBatch的参数和SQL。
- flush时,遍历所有Statement,调用JDBC的executeBatch()。
- MyBatis本身不关心JDBC如何实现批量,只是把所有参数交给JDBC。
JDBC驱动的批量
- executeBatch()的实现由JDBC驱动决定。
- MySQL驱动默认(未加rewriteBatchedStatements)是把每个batch参数单独组装成一条SQL,逐条发送给MySQL服务器。
- 加了rewriteBatchedStatements=true后,驱动会把所有参数合并成一条多值SQL,一次性发送给MySQL服务器。
13.2. 加不加rewriteBatchedStatements的区别
未加时(默认)
- MyBatis flush时,JDBC驱动会发送N条单条SQL到MySQL(N=addBatch次数)。
- 网络交互N次,MySQL解析N次,性能一般。 加了rewriteBatchedStatements=true
- MyBatis flush时,JDBC驱动会把所有参数合成一条大SQL(如多值insert),只发送1次到MySQL。
- 网络交互1次,MySQL只解析1次,性能极高。
举例:假设批量插入1000条数据:
- 未加:MyBatis -> JDBC -> MySQL:1000条单条insert
- 加了:MyBatis -> JDBC -> MySQL:1条多值insert
13.3. 源码/行为佐证
- MyBatis的BatchExecutor只是调用JDBC的addBatch和executeBatch,并不关心JDBC内部怎么实现批量。
- JDBC驱动(MySQL Connector/J)源码中,只有加了rewriteBatchedStatements=true,才会在executeBatch时合并SQL。
13.4 代码示例
TransactionUtils.doInTxn(sqlsession -> {
PmJobLogMapper j = sqlsession.getMapper(PmJobLogMapper.class);
for (int i = 0; i < 30; i++) {
j.recordLog("e1", i, "e1");
}
sqlsession.commit();
for (int i = 0; i < 30; i++) {
j.recordLog("e2", i, "e2");
}
sqlsession.commit();
}, ExecutorType.BATCH);
临时开启记录mysql所有查询:
-- 开启通用日志
SET GLOBAL general_log = 'ON';
-- 查看日志文件路径
SHOW VARIABLES LIKE 'general_log_file';
不加rewriteBatchedStatements时mysql日志(只示例了第一个for循环的日志)是一条一条的SQL:
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (1, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (2, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (3, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (4, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (5, 'e2', 'e2', NOW())
111 Connect root@localhost on logistics_pms using TCP/IP
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (6, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (7, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (8, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (9, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (10, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (11, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (12, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (13, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (14, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (15, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (16, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (17, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (18, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (19, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (20, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (21, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (22, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (23, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (24, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (25, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (26, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (27, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (28, 'e2', 'e2', NOW())
99 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (29, 'e2', 'e2', NOW())
加了rewriteBatchedStatements时mysql日志(只示例了第一个for循环的日志)是一条合并的SQL:
250718 10:05:25 127 Query INSERT INTO pm_job_log
(job_type_code, job_type_name, err_log, create_time)
VALUES (0, 'e1', 'e1', NOW()), (1, 'e1', 'e1', NOW()), (2, 'e1', 'e1', NOW()), (3, 'e1', 'e1', NOW()), (4, 'e1', 'e1', NOW()), (5, 'e1', 'e1', NOW()), (6, 'e1', 'e1', NOW()), (7, 'e1', 'e1', NOW()), (8, 'e1', 'e1', NOW()), (9, 'e1', 'e1', NOW()), (10, 'e1', 'e1', NOW()), (11, 'e1', 'e1', NOW()), (12, 'e1', 'e1', NOW()), (13, 'e1', 'e1', NOW()), (14, 'e1', 'e1', NOW()), (15, 'e1', 'e1', NOW()), (16, 'e1', 'e1', NOW()), (17, 'e1', 'e1', NOW()), (18, 'e1', 'e1', NOW()), (19, 'e1', 'e1', NOW()), (20, 'e1', 'e1', NOW()), (21, 'e1', 'e1', NOW()), (22, 'e1', 'e1', NOW()), (23, 'e1', 'e1', NOW()), (24, 'e1', 'e1', NOW()), (25, 'e1', 'e1', NOW()), (26, 'e1', 'e1', NOW()), (27, 'e1', 'e1', NOW()), (28, 'e1', 'e1', NOW()), (29, 'e1', 'e1', NOW())
13.5. 结论
- MyBatis的批量只是把参数交给JDBC,JDBC怎么发SQL取决于驱动参数。
- 加了rewriteBatchedStatements=true,MyBatis批量才能真正变成MySQL的高效批量。
- 不加,MyBatis批量只是“Java端批量”,不是“数据库端批量”。