问题描述:
软件环境:Spring Boot版本: 2.0.2.RELEASE、MyBatis版本: 3.4.6、Druid版本: 1.1.8、: mysql-connector-java-5.1.46
Spring Boot + MyBatis + Druid 整合中使用批处理的 SqlSessionTemplate 当在Service层抛出RuntimeException(或子类)时,
Druid代理的连接类com.alibaba.druid.pool.DruidPooledConnection的rollback并不会调用目标数据库连接(比如MySql的数据库连接)rollback方法,
而Druid代理的DruidPooledConnection的close方法的逻辑并不是关闭实际的数据库连接,而是将其引用计数器减一,然后将连接重新添加到空闲连接池中,
这个时候因为没有调用rollback, 当其它线程获取到这个连接的时候不是一个纯净的Connection,它里面有上次未提交的sql语句,如果此时提交将会把上次回滚的sql和当前操作的sql一起提交,这个操作很危险的。
定位问题,分析过程如下:
MyBatis默认的SqlSession是SIMPLE, 当配置了:
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
//return new SqlSessionTemplate(sqlSessionFactory); // 不使用批处理 SqlSession, 推荐用这种方式
return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH); // 使用批处理 SqlSession, 不推荐用这种方式, 如果主键自增, 批处理方式无法获取到自增的id
}
这段代码后, 内部使用的 Executor 是 org.apache.ibatis.executor.BatchExecutor, 而不是默认的 org.apache.ibatis.executor.SimpleExecutor
对数据库进行DML之后, 最终都会转到 BatchExecutor 或 SimpleExecutor 的 doUpdate(MappedStatement ms, Object parameterObject) 方法 [注: 根据配置选取是BatchExecutor还是SimpleExecutor]
1、先看能正常回滚数据的情况,也就是 SimpleExecutor, 它的doUpdate(MappedStatement ms, Object parameterObject) 方法, 内部包含如下代码:
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
// 省略其它代码
int count = handler.update(stmt); // handler 的具体类型是: org.apache.ibatis.executor.statement.RoutingStatementHandler , 我们跟踪进入这个方法
// 省略其它代码
}
RoutingStatementHandler.java 的 update 方法如下:
public int update(Statement statement) throws SQLException {
return delegate.update(statement); // delegate 的具体类型是: org.apache.ibatis.executor.statement.PreparedStatementHandler , 我们跟踪进入这个方法
}
PreparedStatementHandler.java 的 update 方法如下:
@Override
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// ps.execute() 这行代码是关键: ps的具体实现类是: com.alibaba.druid.pool.DruidPooledPreparedStatement , 进入这个方法
ps.execute();
// 省略其它代码
}
DruidPooledPreparedStatement.java 的 execute() 方法如下:
@Override
public boolean execute() throws SQLException {
// 省略其它代码
transactionRecord(sql); // 这行代码是关键, 跟进去
// 省略其它代码
}
DruidPooledPreparedStatement.java 的 transactionRecord 方法如下:
protected void transactionRecord(String sql) throws SQLException {
// 触发Connection创建事务
conn.transactionRecord(sql); // conn 的具体实现类是: com.alibaba.druid.pool.DruidPooledConnection , 跟进去查看这个方法
}
DruidPooledConnection.java
protected void transactionRecord(String sql) throws SQLException {
// 这个非空判断是关键: 实际调试的结果是进入了这个判断, 也就是 new 了一个 TransactionInfo
if (transactionInfo == null && (!conn.getAutoCommit())) {
DruidAbstractDataSource dataSource = holder.getDataSource();
dataSource.incrementStartTransactionCount();
transactionInfo = new TransactionInfo(dataSource.createTransactionId()); // 进入了这里, 创建了 TransactionInfo
}
// 省略其它代码
}
以上流程结束后, 当Service抛出RuntimeException 时需要回滚事务,进入 DruidPooledConnection 的 rollback 方法,
public void rollback() throws SQLException {
// 此时 transactionInfo 和 holder 都不为空, 没有进入下面的两个中断判断中, 事务正常回滚
if (transactionInfo == null) { // 中断判断, SimpleExecutor 没有进入此判断
return;
}
if (holder == null) { // 中断判断, SimpleExecutor 没有进入此判断
return;
}
// 省略其它代码
conn.rollback(); // 这里调用实际的数据库连接回滚事务, 事务正常回滚
// 省略其它代码
}
SimpleExecutor 分析完毕
2、再看 BatchExecutor 不能正常回滚数据的情况,
BatchExecutor.java 的 update 方法如下:
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
// 节省篇幅省略了部分代码
final Statement stmt;
if (sql.equals(currentSql) && ms.equals(currentStatement)) {
// 省略其它代码
} else { // 进入了 esle 分支
Connection connection = getConnection(ms.getStatementLog());
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt); //fix Issues 322
currentSql = sql;
currentStatement = ms;
statementList.add(stmt); // 这里把 Statement 存在集合里面了, 后面一定会有清除集合的代码, 否则将导致内存泄漏
batchResultList.add(new BatchResult(ms, sql, parameterObject));
}
handler.batch(stmt); // handler 的具体实现类型是: org.apache.ibatis.executor.statement.RoutingStatementHandler , 进入这个方法
return BATCH_UPDATE_RETURN_VALUE;
}
RoutingStatementHandler.java 的 batch 方法如下:
public void batch(Statement statement) throws SQLException {
delegate.batch(statement); // delegate 的具体实现类是: org.apache.ibatis.executor.statement.PreparedStatementHandler , 进入这个方法
}
PreparedStatementHandler.java 的 batch 方法如下:
public void batch(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.addBatch();// ps 的实现类是: com.alibaba.druid.pool.DruidPooledPreparedStatement , 进入这个方法
}
DruidPooledPreparedStatement.java 的 addBatch 方法如下:
public void addBatch() throws SQLException {
// 省略其它代码
// 特别注意: 这里的addBatch()和 SimpleExecutor 最终调用 PreparedStatement 的 execute() 方法不同, 这个没有创建 TransactionInfo
// 当以上流程结束后, 当Service抛出RuntimeException 时需要回滚事务,进入 DruidPooledConnection 的 rollback 方法,
stmt.addBatch();
}
DruidPooledConnection.java 的 rollback 方法如下:
public void rollback() throws SQLException {
// 此时 transactionInfo 为空, 进入下面的中断判断中, 从而导致后面的 conn.rollback(); 没有执行
if (transactionInfo == null) { // 中断判断, BatchExecutor 进入了此判断, 从而导致后面的 conn.rollback(); 没有执行
return;
}
if (holder == null) {
return;
}
// 省略其它代码
conn.rollback(); // 使用 BatchExecutor 时不能执行此操作
// 省略其它代码
}
至此: 我们提出的问题产生的原因分析完毕.
尝试解决方案如下(在如下两个类新增了两处代码):
org.apache.ibatis.executor.BatchExecutor.java 修改如下:
public class BatchExecutor extends BaseExecutor {
... ...
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException { // 为 true 是回滚数据, line: 111
try {
List<BatchResult> results = new ArrayList<BatchResult>();
if (isRollback) {
// 调试后新增: 2017-07-02 ======================> 开始 ==================================================================>
// 批处理方式需要清除加入的 Statement 的
if(statementList!=null) {
// 缺一个清除 Statement 中的批处理脚本的逻辑
for(Statement stmt : statementList) {
closeStatement(stmt);
// 清除批处理缓存
stmt.clearBatch();
}
}
// 清除批处理的缓存信息
currentSql = null;
statementList.clear();
batchResultList.clear();
// Connection conn = transaction.getConnection(); // 最好不要再这里关闭数据库连接
// if(conn!=null) {
// conn.close();
// }
// 调试后新增: 2017-07-02 ======================>结束 ==================================================================>
// 返回空集合
return Collections.emptyList();
}
}
... ...
}
com.alibaba.druid.pool.DruidPooledPreparedStatement.java 修改如下:
public class DruidPooledPreparedStatement extends DruidPooledStatement implements PreparedStatement {
... ...
@Override
public void addBatch() throws SQLException { // line: 549
checkOpen();
try {
// 调试后新增: 2017-07-02 ======================> 开始 ==================================================================>
if(null == conn.getTransactionInfo()) {
conn.createTransactionInfo(); // 创建 TransactionInfo , 当执行 DruidPooledConnection 的 rollback 时不会进入中断方法
}
// 调试后新增: 2017-07-02 ======================>结束 ==================================================================>
stmt.addBatch();
} catch (Throwable t) {
throw checkException(t);
}
}
... ...
}
后记:
整体来看,是因为DruidPooledPreparedStatement.java 的 TransactionInfo 没有在 addBatch() 方法中创建,而是延迟到了 executeBatch() 方法调用时才创建,
而 BaseExecutor.java 当出现异常时不执行DruidPooledPreparedStatement的executeBatch()方法,进而引发事务没有被回滚。
网友评论